671 lines
32 KiB
C#
671 lines
32 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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;
|
||
}
|
||
}
|
||
}
|