2026-10-09 04:28:39 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// WLInventory.cs — FR 기존 장비 시스템 로직 이식 (#821)
/ /
// 원본 = FR `NetPlayer.cs`(AddItem · EquipItem · ItemLevelUp · ComposeItem · DowngradeItem) +
// `ComposePopup.cs`(IsMaterialItem · FindComposeItemInfo · FindResultItemJson) + `UIUtil`(강등 반환).
// FR 은 서버/오프라인 4단 패턴이지만 WL 은 서버 없이 **로컬 세이브**(PD 결정 보류 → 가정 · persistentDataPath/WL_Equip.dat)로 돈다.
// 골드 = 섬 코인(IslandManager.Coin · 던전 보상이 들어오는 재화) · 섬이 없으면 세이브 안 예비 골드.
/ /
// 규칙(PD 확인 2026-10-08): 합성 = 본 아이템 1 + 재료 RequiredItemCount(2) = 3개 소모 · 본 아이템이 상위 등급으로 바뀐다.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic ;
using System.IO ;
using Newtonsoft.Json ;
using UnityEngine ;
namespace WL.Equip
{
public static class WLInventory
{
public static WLEquipSaveData Data { get ; private set ; }
public static bool Loaded { get ; private set ; }
public static string LastMessage { get ; private set ; } = "" ;
/// <summary>최근 획득 로그(UI 표시용 · 최신이 앞).</summary>
public static readonly List < string > RecentGains = new List < string > ( ) ;
public static event System . Action Changed ;
static readonly Dictionary < int , Item > s_items = new Dictionary < int , Item > ( ) ;
static bool s_resetDoneThisRun ;
// ── 로드 · 세이브 ──────────────────────────────────────────────────
static string SavePath ( )
{
var cfg = WLEquipSettings . Instance ;
string name = cfg ! = null & & ! string . IsNullOrEmpty ( cfg . saveName ) ? cfg . saveName : "WL_Equip" ;
return Application . persistentDataPath + "/" + name + ".dat" ;
}
public static bool EnsureLoaded ( )
{
if ( Loaded ) return true ;
if ( ! WLEquipTables . EnsureLoaded ( ) ) return false ;
var cfg = WLEquipSettings . Instance ;
string path = SavePath ( ) ;
try
{
if ( cfg ! = null & & cfg . resetSaveEveryStart ! = 0 & & ! s_resetDoneThisRun )
{
s_resetDoneThisRun = true ;
if ( File . Exists ( path ) ) { File . Delete ( path ) ; Log ( "세이브 초기화(실행 1회당 1번) — " + path ) ; }
}
Data = File . Exists ( path ) ? JsonConvert . DeserializeObject < WLEquipSaveData > ( File . ReadAllText ( path ) ) : null ;
}
catch ( System . Exception ex ) { Debug . LogWarning ( "[WL-821] 세이브 읽기 실패 — " + ex . Message ) ; Data = null ; }
if ( Data = = null ) Data = new WLEquipSaveData ( ) ;
if ( Data . items = = null ) Data . items = new List < RawItem > ( ) ;
if ( Data . equipItems = = null ) Data . equipItems = new List < int > ( ) ;
if ( Data . nextId < = 0 ) Data . nextId = 1 ;
s_items . Clear ( ) ;
for ( int i = Data . items . Count - 1 ; i > = 0 ; i - - )
{
var raw = Data . items [ i ] ;
var json = WLEquipTables . FindItemJson ( raw . index ) ;
if ( json = = null | | raw . count < = 0 | | s_items . ContainsKey ( raw . _id ) ) { Data . items . RemoveAt ( i ) ; continue ; }
s_items [ raw . _id ] = new Item ( raw , json ) ;
}
for ( int i = Data . equipItems . Count - 1 ; i > = 0 ; i - - )
if ( ! s_items . ContainsKey ( Data . equipItems [ i ] ) ) Data . equipItems . RemoveAt ( i ) ;
Loaded = true ;
Log ( "로드 — 아이템 " + s_items . Count + " · 장착 " + Data . equipItems . Count + " · " + path ) ;
return true ;
}
public static void Save ( )
{
if ( ! Loaded ) return ;
try { File . WriteAllText ( SavePath ( ) , JsonConvert . SerializeObject ( Data ) ) ; }
catch ( System . Exception ex ) { Debug . LogWarning ( "[WL-821] 세이브 쓰기 실패 — " + ex . Message ) ; }
}
/// <summary>세이브를 지우고 빈 상태로(UI 「세이브 초기화」).</summary>
public static void ResetAll ( )
{
try { var p = SavePath ( ) ; if ( File . Exists ( p ) ) File . Delete ( p ) ; } catch { }
Data = new WLEquipSaveData ( ) ;
s_items . Clear ( ) ;
RecentGains . Clear ( ) ;
Loaded = true ;
Save ( ) ;
LastMessage = "세이브 초기화" ;
Notify ( ) ;
}
static void Notify ( )
{
Save ( ) ;
try { if ( Changed ! = null ) Changed ( ) ; } catch ( System . Exception ex ) { Debug . LogWarning ( "[WL-821] Changed 콜백 예외 — " + ex . Message ) ; }
WLEquipStats . RefreshPc ( ) ;
}
static void Log ( string s )
{
var cfg = WLEquipSettings . Instance ;
if ( cfg ! = null & & cfg . verboseLog ! = 0 ) Debug . Log ( "[WL-821] " + s ) ;
}
// ── 재화 ──────────────────────────────────────────────────────────
public static long GetGold ( )
{
int c ;
if ( WL . Island . WLIslandCoin . TryGet ( out c ) ) return c ;
return Data ! = null ? Data . gold : 0 ;
}
static bool SpendGold ( long amount )
{
if ( amount < = 0 ) return true ;
int c ;
if ( WL . Island . WLIslandCoin . TryGet ( out c ) )
{
if ( c < amount ) return false ;
return WL . Island . WLIslandCoin . Add ( - ( int ) amount ) ;
}
if ( Data . gold < amount ) return false ;
Data . gold - = amount ;
return true ;
}
public static void AddGold ( long amount )
{
if ( amount < = 0 ) return ;
if ( ! WL . Island . WLIslandCoin . Add ( ( int ) amount ) ) Data . gold + = amount ;
}
2026-10-18 15:09:05 +00:00
// ── 열쇠(상자 재화 · PD 2026-10-09 · 가격은 보류) ─────────────────
public static int GetKeys ( ) { return Data ! = null ? Data . keys : 0 ; }
public static void AddKeys ( int n )
{
if ( n < = 0 | | ! EnsureLoaded ( ) ) return ;
Data . keys + = n ;
PushGain ( "열쇠 +" + n ) ;
}
public static bool TrySpendKeys ( int n )
{
if ( n < = 0 ) return true ;
if ( Data = = null | | Data . keys < n ) return false ;
Data . keys - = n ;
return true ;
}
2026-10-09 04:28:39 +00:00
// ── 조회 ──────────────────────────────────────────────────────────
public static Item FindItem ( int id ) { Item it ; return s_items . TryGetValue ( id , out it ) ? it : null ; }
public static Item FindItemAnyByIndex ( int index )
{
foreach ( var it in s_items . Values ) if ( it . itemJson . Index = = index ) return it ;
return null ;
}
public static long GetItemCountByIndex ( int index )
{
var cfg = WLEquipSettings . Instance ;
if ( cfg ! = null & & index = = cfg . goldItemIndex ) return GetGold ( ) ;
long n = 0 ;
foreach ( var it in s_items . Values ) if ( it . itemJson . Index = = index ) n + = it . rawItem . count ;
return n ;
}
public static bool IsEquipped ( int id ) { return Data ! = null & & Data . equipItems . Contains ( id ) ; }
public static Item EquippedInSlot ( eFREquipmentType slot )
{
if ( Data = = null ) return null ;
for ( int i = 0 ; i < Data . equipItems . Count ; i + + )
{
var it = FindItem ( Data . equipItems [ i ] ) ;
if ( it ! = null & & it . itemJson . EquipmentType = = slot ) return it ;
}
return null ;
}
/// <summary>목록(장착 → 장비(등급 높은 순) → 소모품).</summary>
public static List < Item > SortedItems ( )
{
var list = new List < Item > ( s_items . Values ) ;
list . Sort ( ( a , b ) = >
{
bool ea = IsEquipped ( a . rawItem . _id ) , eb = IsEquipped ( b . rawItem . _id ) ;
if ( ea ! = eb ) return ea ? - 1 : 1 ;
bool qa = a . itemJson . IsEquipment , qb = b . itemJson . IsEquipment ;
if ( qa ! = qb ) return qa ? - 1 : 1 ;
if ( qa )
{
int t = a . itemJson . EquipmentType . CompareTo ( b . itemJson . EquipmentType ) ; if ( t ! = 0 ) return t ;
int g = b . itemJson . ItemGrade . CompareTo ( a . itemJson . ItemGrade ) ; if ( g ! = 0 ) return g ;
int gl = b . itemJson . GradeLv . CompareTo ( a . itemJson . GradeLv ) ; if ( gl ! = 0 ) return gl ;
int lv = b . rawItem . level . CompareTo ( a . rawItem . level ) ; if ( lv ! = 0 ) return lv ;
}
int ix = a . itemJson . Index . CompareTo ( b . itemJson . Index ) ; if ( ix ! = 0 ) return ix ;
return a . rawItem . _id . CompareTo ( b . rawItem . _id ) ;
} ) ;
return list ;
}
public static int ItemCount { get { return s_items . Count ; } }
// ── 획득 (FR AddItem / AddItemInternal) ──────────────────────────
/// <summary>아이템 지급. 골드 인덱스는 코인으로. 비스택 장비는 count 개를 각각 1개짜리로 만든다.</summary>
public static void AddItem ( int itemIndex , int level , int count , List < Item > gained = null )
{
if ( count < = 0 ) return ;
if ( ! EnsureLoaded ( ) ) return ;
var cfg = WLEquipSettings . Instance ;
if ( cfg ! = null & & itemIndex = = cfg . goldItemIndex ) { AddGold ( count ) ; PushGain ( "골드 +" + count ) ; return ; }
var json = WLEquipTables . FindItemJson ( itemIndex ) ;
if ( json = = null ) { Log ( "AddItem — 표에 없는 Index " + itemIndex ) ; return ; }
if ( level < = 0 ) level = 1 ;
if ( json . IsStack = = 1 )
{
var exist = FindItemAnyByIndex ( itemIndex ) ;
if ( exist ! = null ) { exist . rawItem . count + = count ; if ( gained ! = null ) gained . Add ( exist ) ; }
else { var it = NewItem ( json , level , count ) ; if ( gained ! = null ) gained . Add ( it ) ; }
PushGain ( json . Item_name + " × " + count ) ;
}
else
{
for ( int i = 0 ; i < count ; i + + )
{
var it = NewItem ( json , level , 1 ) ;
if ( gained ! = null ) gained . Add ( it ) ;
2026-10-18 15:09:05 +00:00
PushGain ( json . Item_name + " [" + WLEquipGrades . Label ( json , false ) + "]" ) ;
2026-10-09 04:28:39 +00:00
}
}
}
static Item NewItem ( ItemJson json , int level , int count )
{
var raw = new RawItem ( json . Index , level , count , Data . nextId + + ) ;
Data . items . Add ( raw ) ;
var it = new Item ( raw , json ) ;
s_items [ raw . _id ] = it ;
return it ;
}
static void PushGain ( string s )
{
RecentGains . Insert ( 0 , s ) ;
if ( RecentGains . Count > 30 ) RecentGains . RemoveAt ( RecentGains . Count - 1 ) ;
}
/// <summary>여러 번 AddItem 한 뒤 한 번만 저장·통지.</summary>
public static void Commit ( string message )
{
LastMessage = message ;
Notify ( ) ;
}
static void UpdateItemCount ( RawItem raw )
{
if ( raw . count < = 0 ) RemoveItem ( raw . _id ) ;
}
public static bool RemoveItem ( int id )
{
Item it ;
if ( ! s_items . TryGetValue ( id , out it ) ) return false ;
Data . equipItems . Remove ( id ) ;
Data . items . Remove ( it . rawItem ) ;
s_items . Remove ( id ) ;
return true ;
}
// ── 장착 (FR EquipItem / UnequipItem) ────────────────────────────
public static bool Equip ( int id )
{
var item = FindItem ( id ) ;
if ( item = = null ) { LastMessage = "아이템 없음" ; return false ; }
if ( ! item . itemJson . IsEquipment ) { LastMessage = "장비가 아닙니다" ; return false ; }
for ( int i = 0 ; i < Data . equipItems . Count ; i + + )
{
var e = FindItem ( Data . equipItems [ i ] ) ;
if ( e ! = null & & e . itemJson . EquipmentType = = item . itemJson . EquipmentType )
{
if ( e = = item ) { LastMessage = "이미 장착 중" ; return false ; }
Data . equipItems . RemoveAt ( i ) ;
break ;
}
}
Data . equipItems . Add ( id ) ;
LastMessage = item . itemJson . Item_name + " 장착" ;
Notify ( ) ;
return true ;
}
public static bool Unequip ( int id )
{
if ( ! Data . equipItems . Remove ( id ) ) { LastMessage = "장착 중이 아닙니다" ; return false ; }
var item = FindItem ( id ) ;
LastMessage = ( item ! = null ? item . itemJson . Item_name : id . ToString ( ) ) + " 해제" ;
Notify ( ) ;
return true ;
}
// ── 강화(레벨업 · FR ItemLevelUp) ─────────────────────────────────
/// <summary>다음 레벨 비용(골드 · 강화석) — 없으면 null.</summary>
public static ItemLevelInfo NextLevelCost ( Item item )
{
if ( item = = null | | ! item . itemJson . IsEquipment ) return null ;
if ( item . rawItem . level > = item . itemJson . ItemMaxLv ) return null ;
var lt = WLEquipTables . FindItemLevelJson ( item . itemJson . ItemLvAbilityID ) ;
if ( lt = = null ) return null ;
return lt . FindLevelInfo ( item . rawItem . level ) ;
}
public static bool ItemLevelUp ( int id , bool all )
{
var item = FindItem ( id ) ;
if ( item = = null ) { LastMessage = "아이템 없음" ; return false ; }
if ( ! item . itemJson . IsEquipment ) { LastMessage = "장비가 아닙니다" ; return false ; }
if ( item . rawItem . level > = item . itemJson . ItemMaxLv ) { LastMessage = "최대 레벨" ; return false ; }
var lt = WLEquipTables . FindItemLevelJson ( item . itemJson . ItemLvAbilityID ) ;
if ( lt = = null ) { LastMessage = "레벨표 없음(" + item . itemJson . ItemLvAbilityID + ")" ; return false ; }
int start = item . rawItem . level ;
string stop = "" ;
for ( int lv = start ; lv < item . itemJson . ItemMaxLv ; lv + + )
{
var info = lt . FindLevelInfo ( lv ) ;
var next = lt . FindLevelInfo ( lv + 1 ) ;
if ( info = = null | | next = = null ) { stop = "표 끝" ; break ; }
if ( GetGold ( ) < info . MoneyCountRequired ) { stop = "골드 부족(" + info . MoneyCountRequired + ")" ; break ; }
Item stone = null ;
if ( info . ItemKeyRequired > 0 & & info . ItemCountRequired > 0 )
{
stone = FindItemAnyByIndex ( info . ItemKeyRequired ) ;
if ( stone = = null | | stone . rawItem . count < info . ItemCountRequired )
{
var sj = WLEquipTables . FindItemJson ( info . ItemKeyRequired ) ;
stop = ( sj ! = null ? sj . Item_name : "강화석" ) + " 부족(" + info . ItemCountRequired + ")" ;
break ;
}
}
if ( ! SpendGold ( info . MoneyCountRequired ) ) { stop = "골드 차감 실패" ; break ; }
if ( stone ! = null ) { stone . rawItem . count - = info . ItemCountRequired ; UpdateItemCount ( stone . rawItem ) ; }
item . rawItem . level = next . Level ;
if ( ! all ) break ;
}
if ( item . rawItem . level = = start ) { LastMessage = "강화 불가 — " + stop ; return false ; }
LastMessage = item . itemJson . Item_name + " Lv " + start + " → " + item . rawItem . level + ( stop ! = "" ? " (" + stop + ")" : "" ) ;
Notify ( ) ;
return true ;
}
/// <summary>FR ReturnItemLevel — 레벨 1 초과분에 쓴 강화석을 돌려준다(골드는 돌려주지 않는다 · FR 동일 「추정」).</summary>
static void RefundLevelMaterials ( Item item )
{
if ( item . rawItem . level < = 1 ) return ;
var lt = WLEquipTables . FindItemLevelJson ( item . itemJson . ItemLvAbilityID ) ;
if ( lt = = null ) return ;
for ( int lv = 1 ; lv < item . rawItem . level ; lv + + )
{
var info = lt . FindLevelInfo ( lv ) ;
if ( info = = null ) continue ;
if ( info . ItemKeyRequired > 0 & & info . ItemCountRequired > 0 ) AddItem ( info . ItemKeyRequired , 1 , info . ItemCountRequired ) ;
}
}
// ── 합성(등급 업 · FR ComposePopup + NetPlayer.ComposeItem) ─────────
public class ComposePlan
{
public Item source ;
public ItemComposeJson compose ;
public ItemJson result ;
public readonly List < Item > materials = new List < Item > ( ) ;
}
/// <summary>FR `IsMaterialItem`(장비 분기 그대로).</summary>
public static bool IsMaterialItem ( Item check , Item source , ItemComposeJson c )
{
if ( check . itemJson . EquipmentType ! = source . itemJson . EquipmentType ) return false ;
if ( c . RequiredGradeLevel ! = check . itemJson . GradeLv ) return false ;
if ( c . SameGroupNo = = 0 )
{
return check . itemJson . ItemGrade = = c . ItemGrade ;
}
if ( check . itemJson . ItemGrade ! = c . ItemGrade ) return false ;
if ( check . itemJson . ItemType = = eFRItemType . EnchantScroll ) return false ;
return check . itemJson . GroupNo = = source . itemJson . GroupNo ;
}
/// <summary>본 아이템 하나에 대한 합성 계획(재료는 장착 중이 아닌 것만 · FR 과 같이 재료끼리는 같은 Index).</summary>
public static ComposePlan PlanCompose ( Item source , HashSet < int > excludeIds = null )
{
if ( source = = null | | ! source . itemJson . IsEquipment ) return null ;
var c = WLEquipTables . FindItemComposeJson ( source . itemJson . ItemGrade , source . itemJson . GradeLv ) ;
if ( c = = null ) return null ;
var result = WLEquipTables . FindResultItemJson ( source . itemJson , c ) ;
if ( result = = null ) return null ;
// 재료 후보를 Index 별로 묶는다(FR: 비스택 재료는 첫 재료와 같은 Index 로 RequiredItemCount 개)
var byIndex = new Dictionary < int , List < Item > > ( ) ;
var order = new List < int > ( ) ;
foreach ( var it in s_items . Values )
{
if ( it = = source ) continue ;
if ( IsEquipped ( it . rawItem . _id ) ) continue ;
if ( excludeIds ! = null & & excludeIds . Contains ( it . rawItem . _id ) ) continue ;
if ( ! IsMaterialItem ( it , source , c ) ) continue ;
List < Item > l ;
if ( ! byIndex . TryGetValue ( it . itemJson . Index , out l ) ) { l = new List < Item > ( ) ; byIndex [ it . itemJson . Index ] = l ; order . Add ( it . itemJson . Index ) ; }
l . Add ( it ) ;
}
// 같은 그룹(같은 이름) 재료를 우선, 그다음 레벨 낮은 것부터
order . Sort ( ( a , b ) = >
{
bool ga = a = = source . itemJson . Index , gb = b = = source . itemJson . Index ;
if ( ga ! = gb ) return ga ? - 1 : 1 ;
return a . CompareTo ( b ) ;
} ) ;
for ( int o = 0 ; o < order . Count ; o + + )
{
var l = byIndex [ order [ o ] ] ;
l . Sort ( ( x , y ) = > x . rawItem . level . CompareTo ( y . rawItem . level ) ) ;
int have = 0 ;
var plan = new ComposePlan { source = source , compose = c , result = result } ;
for ( int i = 0 ; i < l . Count & & have < c . RequiredItemCount ; i + + )
{
int n = l [ i ] . itemJson . IsStack = = 1 ? l [ i ] . rawItem . count : 1 ;
plan . materials . Add ( l [ i ] ) ;
have + = n ;
}
if ( have > = c . RequiredItemCount ) return plan ;
}
return null ;
}
public static bool Compose ( ComposePlan p )
{
if ( p = = null | | p . source = = null ) { LastMessage = "합성 재료 부족" ; return false ; }
int need = p . compose . RequiredItemCount ;
string names = "" ;
for ( int i = 0 ; i < p . materials . Count & & need > 0 ; i + + )
{
var m = p . materials [ i ] ;
if ( m . itemJson . IsStack = = 1 )
{
int take = Mathf . Min ( need , m . rawItem . count ) ;
m . rawItem . count - = take ; need - = take ;
UpdateItemCount ( m . rawItem ) ;
}
else
{
RefundLevelMaterials ( m ) ;
m . rawItem . count = 0 ; need - = 1 ;
UpdateItemCount ( m . rawItem ) ;
}
names + = ( names = = "" ? "" : ", " ) + m . itemJson . Item_name ;
}
2026-10-18 15:09:05 +00:00
string before = WLEquipGrades . Label ( p . source . itemJson , false ) ;
2026-10-09 04:28:39 +00:00
p . source . rawItem . index = p . result . Index ;
p . source . itemJson = p . result ;
if ( p . source . rawItem . level > p . result . ItemMaxLv ) p . source . rawItem . level = p . result . ItemMaxLv ;
2026-10-18 15:09:05 +00:00
string after = WLEquipGrades . Label ( p . result , false ) ;
2026-10-09 04:28:39 +00:00
LastMessage = p . result . Item_name + " " + before + " → " + after + " (재료 " + names + ")" ;
return true ;
}
2026-10-18 15:09:05 +00:00
/// <summary>합성 화면용 — 본 아이템의 합성 행·결과(둘 다 없으면 false).</summary>
public static bool ComposeInfo ( Item source , out ItemComposeJson compose , out ItemJson result )
{
compose = null ; result = null ;
if ( source = = null | | ! source . itemJson . IsEquipment ) return false ;
compose = WLEquipTables . FindItemComposeJson ( source . itemJson . ItemGrade , source . itemJson . GradeLv ) ;
if ( compose = = null ) return false ;
result = WLEquipTables . FindResultItemJson ( source . itemJson , compose ) ;
return result ! = null ;
}
/// <summary>합성 화면용 — 본 아이템에 쓸 수 있는 재료 후보(장착 중 제외 · 본 아이템 제외). 같은 그룹 → 레벨 낮은 순.</summary>
public static List < Item > MaterialCandidates ( Item source )
{
var list = new List < Item > ( ) ;
ItemComposeJson c ; ItemJson r ;
if ( ! ComposeInfo ( source , out c , out r ) ) return list ;
foreach ( var it in s_items . Values )
{
if ( it = = source | | IsEquipped ( it . rawItem . _id ) ) continue ;
if ( IsMaterialItem ( it , source , c ) ) list . Add ( it ) ;
}
list . Sort ( ( a , b ) = >
{
bool ga = a . itemJson . Index = = source . itemJson . Index , gb = b . itemJson . Index = = source . itemJson . Index ;
if ( ga ! = gb ) return ga ? - 1 : 1 ;
int ix = a . itemJson . Index . CompareTo ( b . itemJson . Index ) ; if ( ix ! = 0 ) return ix ;
int lv = a . rawItem . level . CompareTo ( b . rawItem . level ) ; if ( lv ! = 0 ) return lv ;
return a . rawItem . _id . CompareTo ( b . rawItem . _id ) ;
} ) ;
return list ;
}
/// <summary>재료 목록이 채우는 개수(스택은 count · 비스택은 1개씩).</summary>
public static int MaterialCount ( List < Item > mats )
{
int n = 0 ;
if ( mats = = null ) return 0 ;
for ( int i = 0 ; i < mats . Count ; i + + ) n + = mats [ i ] . itemJson . IsStack = = 1 ? mats [ i ] . rawItem . count : 1 ;
return n ;
}
/// <summary>합성 화면(FR ComposePopup 흐름) — 사용자가 고른 재료로 합성. FR 재료 규칙(같은 Index 끼리)은 PlanCompose 의 자동 선택에만 두고 여기서는 IsMaterialItem 만 본다.</summary>
public static bool ComposeWith ( int sourceId , List < int > materialIds )
{
var src = FindItem ( sourceId ) ;
ItemComposeJson c ; ItemJson r ;
if ( ! ComposeInfo ( src , out c , out r ) ) { LastMessage = "합성 불가 — 최고 등급이거나 합성표에 없음" ; return false ; }
var plan = new ComposePlan { source = src , compose = c , result = r } ;
if ( materialIds ! = null )
for ( int i = 0 ; i < materialIds . Count ; i + + )
{
var m = FindItem ( materialIds [ i ] ) ;
if ( m = = null | | m = = src | | IsEquipped ( m . rawItem . _id ) | | ! IsMaterialItem ( m , src , c ) ) { LastMessage = "재료가 아닙니다" ; return false ; }
if ( ! plan . materials . Contains ( m ) ) plan . materials . Add ( m ) ;
}
int have = MaterialCount ( plan . materials ) ;
if ( have < c . RequiredItemCount ) { LastMessage = "재료 부족 — " + have + "/" + c . RequiredItemCount ; return false ; }
bool ok = Compose ( plan ) ;
if ( ok ) Notify ( ) ;
return ok ;
}
2026-10-09 04:28:39 +00:00
public static bool ComposeOne ( int sourceId )
{
var src = FindItem ( sourceId ) ;
var plan = PlanCompose ( src ) ;
if ( plan = = null ) { LastMessage = "합성 불가 — 같은 등급·부위 재료 " + ( src ! = null ? ReqCountText ( src ) : "?" ) + "개 필요" ; return false ; }
bool ok = Compose ( plan ) ;
if ( ok ) Notify ( ) ;
return ok ;
}
static string ReqCountText ( Item src )
{
var c = WLEquipTables . FindItemComposeJson ( src . itemJson . ItemGrade , src . itemJson . GradeLv ) ;
return c ! = null ? c . RequiredItemCount . ToString ( ) : "-" ;
}
/// <summary>전부 합성(FR 「모두 합성」) — 장착 중인 것부터 본 아이템으로, 더 안 될 때까지 반복.</summary>
public static int ComposeAll ( )
{
int done = 0 ;
for ( int guard = 0 ; guard < 500 ; guard + + )
{
ComposePlan plan = null ;
foreach ( var it in SortedItems ( ) )
{
if ( ! it . itemJson . IsEquipment ) continue ;
plan = PlanCompose ( it ) ;
if ( plan ! = null ) break ;
}
if ( plan = = null ) break ;
if ( ! Compose ( plan ) ) break ;
done + + ;
}
LastMessage = done > 0 ? "전부 합성 — " + done + "회" : "합성할 조합 없음" ;
if ( done > 0 ) Notify ( ) ;
return done ;
}
// ── 강등(FR DowngradeItem · 한 단계) ───────────────────────────────
public static bool Downgrade ( int id )
{
var item = FindItem ( id ) ;
if ( item = = null | | ! item . itemJson . IsEquipment ) { LastMessage = "장비가 아닙니다" ; return false ; }
var c = WLEquipTables . FindComposeByResult ( item . itemJson . ItemGrade , item . itemJson . GradeLv ) ;
if ( c = = null ) { LastMessage = "최하 등급 — 강등 불가" ; return false ; }
var result = WLEquipTables . FindGroupItem ( item . itemJson . GroupNo , c . ItemGrade , c . RequiredGradeLevel ) ;
if ( result = = null ) { LastMessage = "강등 결과 아이템 없음" ; return false ; }
2026-10-18 15:09:05 +00:00
string before = WLEquipGrades . Label ( item . itemJson , false ) ;
2026-10-09 04:28:39 +00:00
item . rawItem . index = result . Index ;
item . itemJson = result ;
if ( item . rawItem . level > result . ItemMaxLv ) item . rawItem . level = result . ItemMaxLv ;
// 재료 반환 — 같은 그룹의 재료 등급 아이템 RequiredItemCount 개(FR UIUtil 「현재 등급까지 지급」의 한 단계 판)
AddItem ( result . Index , 1 , c . RequiredItemCount ) ;
2026-10-18 15:09:05 +00:00
LastMessage = result . Item_name + " " + before + " → " + WLEquipGrades . Label ( result , false ) + " (재료 " + c . RequiredItemCount + "개 반환)" ;
2026-10-09 04:28:39 +00:00
Notify ( ) ;
return true ;
}
// ── 설명 ───────────────────────────────────────────────────────────
public static string Describe ( Item it )
{
if ( it = = null ) return "" ;
var j = it . itemJson ;
var sb = new System . Text . StringBuilder ( ) ;
2026-10-18 15:09:05 +00:00
sb . Append ( j . Item_name ) . Append ( " [" ) . Append ( WLEquipGrades . Label ( j , true ) ) . Append ( "] " ) ;
2026-10-09 04:28:39 +00:00
if ( j . IsEquipment )
{
sb . Append ( WLEquipTables . SlotName ( j . EquipmentType ) ) . Append ( " Lv " ) . Append ( it . rawItem . level ) . Append ( '/' ) . Append ( j . ItemMaxLv ) ;
if ( IsEquipped ( it . rawItem . _id ) ) sb . Append ( " (장착 중)" ) ;
sb . Append ( '\n' ) ;
var cost = NextLevelCost ( it ) ;
if ( cost ! = null )
{
var sj = WLEquipTables . FindItemJson ( cost . ItemKeyRequired ) ;
sb . Append ( "\n다음 강화: 골드 " ) . Append ( cost . MoneyCountRequired ) ;
if ( cost . ItemKeyRequired > 0 ) sb . Append ( " + " ) . Append ( sj ! = null ? sj . Item_name : "강화석" ) . Append ( ' ' ) . Append ( cost . ItemCountRequired )
. Append ( " (보유 " ) . Append ( GetItemCountByIndex ( cost . ItemKeyRequired ) ) . Append ( ')' ) ;
}
else sb . Append ( "\n최대 레벨" ) ;
var comp = WLEquipTables . FindItemComposeJson ( j . ItemGrade , j . GradeLv ) ;
if ( comp ! = null )
{
var res = WLEquipTables . FindResultItemJson ( j , comp ) ;
2026-10-18 15:09:05 +00:00
sb . Append ( "\n합성: 같은 " ) . Append ( comp . SameGroupNo = = 1 ? "아이템" : "등급·부위" ) . Append ( " " ) . Append ( WLEquipGrades . LabelOf ( comp . ItemGrade , comp . RequiredGradeLevel , true ) )
. Append ( " 재료 " ) . Append ( comp . RequiredItemCount ) . Append ( "개 → " )
. Append ( res ! = null ? WLEquipGrades . Label ( res , true ) : "?" ) ;
2026-10-09 04:28:39 +00:00
}
else sb . Append ( "\n합성: 최고 등급" ) ;
2026-10-18 15:09:05 +00:00
// 등급 7단계 옵션(PD 2026-10-09) — 기본 수치 + 주스탯 × 등급 % · 상한 없음
string formula = WLEquipStats . FormulaText ( it ) ;
if ( ! string . IsNullOrEmpty ( formula ) ) sb . Append ( '\n' ) . Append ( formula ) ;
var cfgL = WLEquipSettings . Instance ;
if ( cfgL ! = null & & cfgL . applyLegacyFrOptions ! = 0 )
2026-10-09 04:28:39 +00:00
{
2026-10-18 15:09:05 +00:00
sb . Append ( "\n구 FR 옵션(등급별 개방):" ) ;
var oj = WLEquipTables . FindItemOptionJson ( j . GroupNo ) ;
if ( oj = = null ) sb . Append ( " 없음" ) ;
else
2026-10-09 04:28:39 +00:00
{
2026-10-18 15:09:05 +00:00
for ( int i = 0 ; i < oj . ItemOptions . Count ; i + + )
2026-10-09 04:28:39 +00:00
{
2026-10-18 15:09:05 +00:00
var e = oj . ItemOptions [ i ] ;
bool open = e . ItemGrade < = j . ItemGrade ;
sb . Append ( "\n " ) . Append ( open ? "● " : "○ " ) . Append ( WLEquipTables . GradeName ( e . ItemGrade ) ) . Append ( ": " ) ;
for ( int a = 0 ; a < e . Abilities . Count ; a + + )
{
var ab = e . Abilities [ a ] ;
sb . Append ( WLEquipTables . AbilityName ( ab . Ability ) ) . Append ( ' ' ) . Append ( ab . AbilityValue ) ;
sb . Append ( WLEquipStats . IsMapped ( ab . Ability ) ? "" : "(미적용)" ) ;
if ( a < e . Abilities . Count - 1 ) sb . Append ( ", " ) ;
}
if ( e . ConditionalAbilities . Count > 0 ) sb . Append ( e . Abilities . Count > 0 ? ", " : "" ) . Append ( "조건부 " ) . Append ( e . ConditionalAbilities . Count ) . Append ( "(미적용)" ) ;
2026-10-09 04:28:39 +00:00
}
}
}
}
else
{
sb . Append ( "× " ) . Append ( it . rawItem . count ) ;
if ( ! string . IsNullOrEmpty ( j . Desc ) ) sb . Append ( '\n' ) . Append ( j . Desc ) ;
}
return sb . ToString ( ) ;
}
public static string RowText ( Item it )
{
var j = it . itemJson ;
2026-10-18 15:09:05 +00:00
string g = "[" + WLEquipGrades . Label ( j , true ) + "]" ;
2026-10-09 04:28:39 +00:00
if ( j . IsEquipment )
return ( IsEquipped ( it . rawItem . _id ) ? "E " : " " ) + WLEquipTables . SlotName ( j . EquipmentType ) + " " + j . Item_name + " " + g + " Lv" + it . rawItem . level ;
return " " + j . Item_name + " × " + it . rawItem . count ;
}
}
}