Project_WL/Assets/WL/Equip/WLInventory.cs

671 lines
32 KiB
C#
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
// ── 열쇠(상자 재화 · 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;
}
// ── 조회 ──────────────────────────────────────────────────────────
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);
PushGain(json.Item_name + " [" + WLEquipGrades.Label(json, false) + "]");
}
}
}
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;
}
string before = WLEquipGrades.Label(p.source.itemJson, false);
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;
string after = WLEquipGrades.Label(p.result, false);
LastMessage = p.result.Item_name + " " + before + " → " + after + " (재료 " + names + ")";
return true;
}
/// <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;
}
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; }
string before = WLEquipGrades.Label(item.itemJson, false);
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);
LastMessage = result.Item_name + " " + before + " → " + WLEquipGrades.Label(result, false) + " (재료 " + c.RequiredItemCount + "개 반환)";
Notify();
return true;
}
// ── 설명 ───────────────────────────────────────────────────────────
public static string Describe(Item it)
{
if (it == null) return "";
var j = it.itemJson;
var sb = new System.Text.StringBuilder();
sb.Append(j.Item_name).Append(" [").Append(WLEquipGrades.Label(j, true)).Append("] ");
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);
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) : "?");
}
else sb.Append("\n합성: 최고 등급");
// 등급 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)
{
sb.Append("\n구 FR 옵션(등급별 개방):");
var oj = WLEquipTables.FindItemOptionJson(j.GroupNo);
if (oj == null) sb.Append(" 없음");
else
{
for (int i = 0; i < oj.ItemOptions.Count; i++)
{
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("(미적용)");
}
}
}
}
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;
string g = "[" + WLEquipGrades.Label(j, true) + "]";
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;
}
}
}