Project_WL/Assets/WL/Equip/WLInventory.cs

671 lines
32 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
}
}