// ───────────────────────────────────────────────────────────────────────────── // WLEquipTables.cs — FR 장비 테이블 로더 (#821) // // FR 은 xlsm → json → Cook(prefab) 3단이지만 WL 에서는 **json 을 Resources 에서 바로** 읽는다. // FR 표는 엑셀 셀 병합 때문에 묶음의 첫 행에만 Index 가 있다 → FR `ItemLevelTable.LoadFromText` · // `JsonUtil.ReadItemDropPackageJson` · `ItemOptionTable.LoadFromText` 의 묶기 규칙을 그대로 재현한다. // 파일 이름은 FR 원본 그대로(`WLEquipSettings.tableFolder` 아래). // ───────────────────────────────────────────────────────────────────────────── using System.Collections.Generic; using Newtonsoft.Json; using UnityEngine; namespace WL.Equip { public static class WLEquipTables { public static bool Loaded { get; private set; } public static string LastLog { get; private set; } = ""; static readonly Dictionary s_items = new Dictionary(); static readonly List s_itemList = new List(); static readonly Dictionary s_levels = new Dictionary(); static readonly Dictionary s_compose = new Dictionary(); static readonly Dictionary s_options = new Dictionary(); static readonly Dictionary s_dropGroups = new Dictionary(); static readonly Dictionary s_dropPackages = new Dictionary(); public static IReadOnlyList Items { get { EnsureLoaded(); return s_itemList; } } public static int ItemCount { get { return s_items.Count; } } public static int LevelTableCount { get { return s_levels.Count; } } public static int OptionCount { get { return s_options.Count; } } public static bool EnsureLoaded() { if (Loaded) return true; var cfg = WLEquipSettings.Instance; string folder = cfg != null ? cfg.tableFolder : "WL/FR"; try { LoadItems(folder + "/ItemTable"); LoadCompose(folder + "/ItemComposeTable"); LoadOptions(folder + "/ItemOptionTable"); string[] levelFiles = { "Item_Weapon", "Item_Necklace", "Item_Gloves", "Item_Amor", "Item_Belt", "Item_Shoes" }; for (int i = 0; i < levelFiles.Length; i++) LoadLevels(folder + "/" + levelFiles[i]); LoadDropGroups(folder + "/ItemDropGroupTable"); LoadDropPackages(folder + "/ItemDropPackageTable"); Loaded = true; LastLog = "FR 표 로드 — 아이템 " + s_items.Count + " · 레벨표 " + s_levels.Count + " · 합성 " + s_compose.Count + " · 옵션 " + s_options.Count + " · 드랍그룹 " + s_dropGroups.Count + " · 드랍패키지 " + s_dropPackages.Count; if (cfg != null && cfg.verboseLog != 0) Debug.Log("[WL-821] " + LastLog); } catch (System.Exception ex) { LastLog = "FR 표 로드 실패 — " + ex.Message; Debug.LogError("[WL-821] " + LastLog); } return Loaded; } static string ReadText(string path) { var ta = Resources.Load(path); if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + path + ".json 없음"); return ta.text; } public static int ExtraItems { get; private set; } static void LoadItems(string path) { s_items.Clear(); s_itemList.Clear(); ExtraItems = 0; var arr = JsonConvert.DeserializeObject(ReadText(path)); for (int i = 0; i < arr.Length; i++) { if (arr[i].Index <= 0) continue; if (s_items.ContainsKey(arr[i].Index)) continue; s_items.Add(arr[i].Index, arr[i]); s_itemList.Add(arr[i]); } // #825 WL 덧씌움 — FR 표에 없는 무기 그룹(창 9006·9007 · PD 「6종」)을 Resources/WL/WLItemTableExtra.json 에서 보탠다(FR 원본 파일 무수정) var extra = Resources.Load("WL/WLItemTableExtra"); if (extra != null) { try { var more = JsonConvert.DeserializeObject(extra.text); for (int i = 0; more != null && i < more.Length; i++) { if (more[i].Index <= 0 || s_items.ContainsKey(more[i].Index)) continue; s_items.Add(more[i].Index, more[i]); s_itemList.Add(more[i]); ExtraItems++; } } catch (System.Exception ex) { Debug.LogWarning("[WL-825] 장비 표 덧씌움 실패 — " + ex.Message); } } // #879 PD 10-16 벨트 슬롯 → 반지 — 표시 이름만 바꾼다(FR 원본 파일 무수정 · 규칙 = WLPartStats.json renameItems) try { WL.PartSkill.WLPartStats.RenameItems(s_itemList); } catch (System.Exception ex) { Debug.LogWarning("[WL-879] 부위 이름 바꾸기 실패 — " + ex.Message); } } static void LoadCompose(string path) { s_compose.Clear(); var arr = JsonConvert.DeserializeObject(ReadText(path)); for (int i = 0; i < arr.Length; i++) if (!s_compose.ContainsKey(arr[i].Index)) s_compose.Add(arr[i].Index, arr[i]); } static void LoadOptions(string path) { s_options.Clear(); var readers = JsonConvert.DeserializeObject(ReadText(path)); ItemOptionJson cur = null; for (int i = 0; i < readers.Length; i++) { var r = readers[i]; if (cur == null || (r.Index != 0 && cur.Index != r.Index)) { cur = new ItemOptionJson(r.Index, r.Desc); if (!s_options.ContainsKey(cur.Index)) s_options.Add(cur.Index, cur); } cur.ItemOptions.Add(new ItemOptionElement(r)); } } static void LoadLevels(string path) { var infos = JsonConvert.DeserializeObject(ReadText(path)); ItemLevelJson cur = null; for (int i = 0; i < infos.Length; i++) { int idx = infos[i].Index; if (cur == null || (idx != 0 && cur.Index != idx)) { cur = new ItemLevelJson(idx); if (!s_levels.ContainsKey(idx)) s_levels.Add(idx, cur); else cur = s_levels[idx]; } else if (idx == 0) infos[i].Index = cur.Index; cur.LevelInfos.Add(infos[i]); } } static void LoadDropGroups(string path) { s_dropGroups.Clear(); var infos = JsonConvert.DeserializeObject(ReadText(path)); ItemDropGroupJson cur = null; for (int i = 0; i < infos.Length; i++) { int idx = infos[i].Index; if (cur == null || (idx != 0 && cur.Index != idx)) { cur = new ItemDropGroupJson(idx); if (!s_dropGroups.ContainsKey(idx)) s_dropGroups.Add(idx, cur); else cur = s_dropGroups[idx]; } else if (idx == 0) infos[i].Index = cur.Index; cur.ItemDropGroupInfos.Add(infos[i]); } } static void LoadDropPackages(string path) { s_dropPackages.Clear(); var infos = JsonConvert.DeserializeObject(ReadText(path)); ItemDropPackageJson cur = null; for (int i = 0; i < infos.Length; i++) { int idx = infos[i].Index; if (cur == null || (idx != 0 && cur.Index != idx)) { cur = new ItemDropPackageJson(idx); if (!s_dropPackages.ContainsKey(idx)) s_dropPackages.Add(idx, cur); else cur = s_dropPackages[idx]; } else if (idx == 0) infos[i].Index = cur.Index; cur.ItemDropPackageInfos.Add(infos[i]); cur.TotalRate += infos[i].Rate; } } // ── 조회 ─────────────────────────────────────────────────────────── public static ItemJson FindItemJson(int index) { EnsureLoaded(); ItemJson j; return s_items.TryGetValue(index, out j) ? j : null; } public static ItemLevelJson FindItemLevelJson(int index) { EnsureLoaded(); ItemLevelJson j; return s_levels.TryGetValue(index, out j) ? j : null; } public static ItemOptionJson FindItemOptionJson(int groupNo) { EnsureLoaded(); ItemOptionJson j; return s_options.TryGetValue(groupNo, out j) ? j : null; } public static ItemDropGroupJson FindDropGroup(int index) { EnsureLoaded(); ItemDropGroupJson j; return s_dropGroups.TryGetValue(index, out j) ? j : null; } public static ItemDropPackageJson FindDropPackage(int index) { EnsureLoaded(); ItemDropPackageJson j; return s_dropPackages.TryGetValue(index, out j) ? j : null; } /// FR `ItemComposeTable.FindItemComposeJson` — 키 = 등급×1000 + 등급 단계. public static ItemComposeJson FindItemComposeJson(eFRItemGrade grade, int gradeLv) { EnsureLoaded(); ItemComposeJson j; return s_compose.TryGetValue((int)grade * 1000 + gradeLv, out j) ? j : null; } /// 강등용 역조회 — 결과(등급·단계)가 주어진 값인 합성 행. public static ItemComposeJson FindComposeByResult(eFRItemGrade resultGrade, int resultGradeLv) { EnsureLoaded(); foreach (var kv in s_compose) if (kv.Value.ResultGrade == resultGrade && kv.Value.ResultGradeLevel == resultGradeLv) return kv.Value; return null; } /// FR `ComposePopup.FindResultItemJson` — 같은 그룹에서 결과 등급·단계의 장비. public static ItemJson FindResultItemJson(ItemJson source, ItemComposeJson compose) { return FindGroupItem(source.GroupNo, compose.ResultGrade, compose.ResultGradeLevel); } public static ItemJson FindGroupItem(int groupNo, eFRItemGrade grade, int gradeLv) { EnsureLoaded(); for (int i = 0; i < s_itemList.Count; i++) { var j = s_itemList[i]; if (j.ItemType != eFRItemType.Equipment) continue; if (j.GroupNo == groupNo && j.ItemGrade == grade && j.GradeLv == gradeLv) return j; } return null; } /// 표의 등급 옵션 중 지금 등급까지 열린 것(FR `PlayerManager.GetTotalAbilities` 규칙 — 등급 오름차순, 초과 시 중단). public static void CollectOpenedOptions(ItemJson item, List into) { var oj = FindItemOptionJson(item.GroupNo); if (oj == null) return; for (int i = 0; i < oj.ItemOptions.Count; i++) { var e = oj.ItemOptions[i]; if (e == null) continue; if (e.ItemGrade > item.ItemGrade) break; into.Add(e); } } // ── 드랍 (FR `Data.GetRandomDropPackageInfoInGroup` + `ItemDropPackageTable.GetRandomDropPackageInfo`) ── public struct Drop { public int itemId; public int count; } public static void RollDropGroup(int groupId, List into, System.Random rng) { var g = FindDropGroup(groupId); if (g == null) return; for (int i = 0; i < g.ItemDropGroupInfos.Count; i++) { var gi = g.ItemDropGroupInfos[i]; if (rng.Next(1, 10001) > gi.Rate) continue; var pkg = FindDropPackage(gi.Package_ID); if (pkg == null) continue; if (gi.DropType == eFRDropType.All) { for (int d = 0; d < pkg.ItemDropPackageInfos.Count; d++) into.Add(ToDrop(pkg.ItemDropPackageInfos[d], rng)); } else { var p = RollPackage(pkg, rng); if (p != null) into.Add(ToDrop(p, rng)); } } } public static ItemDropPackageInfo RollPackage(ItemDropPackageJson pkg, System.Random rng) { if (pkg == null || pkg.TotalRate <= 0) return null; int r = rng.Next(1, pkg.TotalRate + 1); for (int i = 0; i < pkg.ItemDropPackageInfos.Count; i++) { r -= pkg.ItemDropPackageInfos[i].Rate; if (r <= 0) return pkg.ItemDropPackageInfos[i]; } return null; } static Drop ToDrop(ItemDropPackageInfo p, System.Random rng) { int min = p.MinCount <= 0 ? 1 : p.MinCount; int max = p.MaxCount < min ? min : p.MaxCount; return new Drop { itemId = p.Item_ID, count = rng.Next(min, max + 1) }; } public static string GradeName(eFRItemGrade g) { switch (g) { case eFRItemGrade.Normal: return "일반"; case eFRItemGrade.Magic: return "매직"; case eFRItemGrade.Rare: return "레어"; case eFRItemGrade.Elite: return "엘리트"; case eFRItemGrade.Epic: return "에픽"; case eFRItemGrade.Legend: return "전설"; } return g.ToString(); } public static string SlotName(eFREquipmentType t) { // #879 부위 이름 = 등급 표 parts[].name(데이터 · 벨트 → 반지) · 표가 없을 때만 아래 기본값 var p = WLEquipGrades.Loaded || WLEquipGrades.EnsureLoaded() ? WLEquipGrades.Part(t) : null; if (p != null && !string.IsNullOrEmpty(p.name)) return p.name; switch (t) { case eFREquipmentType.Weapon: return "무기"; case eFREquipmentType.Necklace: return "목걸이"; case eFREquipmentType.Glove: return "장갑"; case eFREquipmentType.Armor: return "갑옷"; case eFREquipmentType.Belt: return "벨트"; case eFREquipmentType.Shose: return "신발"; } return t.ToString(); } public static string AbilityName(eFRAbility a) { switch (a) { case eFRAbility.ItemRange: return "획득 범위 %"; case eFRAbility.Health: return "체력 %"; case eFRAbility.RecoveryHealth: return "체력 회복 %"; case eFRAbility.Speed: return "이동 속도 %"; case eFRAbility.Defence: return "피해 감소 %"; case eFRAbility.Attack: return "공격력 %"; case eFRAbility.IncreaseDrop: return "획득량 %"; case eFRAbility.Rolling: return "구르기 %"; case eFRAbility.ProjectileSpeed: return "투사체 속도 %"; case eFRAbility.SkillRange: return "스킬 범위 %"; case eFRAbility.SkillDuration: return "스킬 지속 %"; case eFRAbility.Cooltime: return "쿨타임 감소 %"; case eFRAbility.AttackDefence: return "공격·방어 %"; case eFRAbility.Passive: return "패시브 효과 %"; case eFRAbility.Critical: return "치명타 확률 %"; case eFRAbility.CriticalDamage: return "치명타 피해 %"; case eFRAbility.Revive: return "부활 횟수"; case eFRAbility.HeartRecover: return "하트 회복 %"; case eFRAbility.SpeedValue: return "이동 속도 +"; case eFRAbility.Excellent: return "액설런트 확률 %"; case eFRAbility.ExcellentDamage: return "액설런트 피해 %"; } return a.ToString(); } } }