340 lines
17 KiB
C#
340 lines
17 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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<int, ItemJson> s_items = new Dictionary<int, ItemJson>();
|
||
static readonly List<ItemJson> s_itemList = new List<ItemJson>();
|
||
static readonly Dictionary<int, ItemLevelJson> s_levels = new Dictionary<int, ItemLevelJson>();
|
||
static readonly Dictionary<int, ItemComposeJson> s_compose = new Dictionary<int, ItemComposeJson>();
|
||
static readonly Dictionary<int, ItemOptionJson> s_options = new Dictionary<int, ItemOptionJson>();
|
||
static readonly Dictionary<int, ItemDropGroupJson> s_dropGroups = new Dictionary<int, ItemDropGroupJson>();
|
||
static readonly Dictionary<int, ItemDropPackageJson> s_dropPackages = new Dictionary<int, ItemDropPackageJson>();
|
||
|
||
public static IReadOnlyList<ItemJson> 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<TextAsset>(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<ItemJson[]>(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<TextAsset>("WL/WLItemTableExtra");
|
||
if (extra != null)
|
||
{
|
||
try
|
||
{
|
||
var more = JsonConvert.DeserializeObject<ItemJson[]>(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<ItemComposeJson[]>(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<ItemOptionReader[]>(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<ItemLevelInfo[]>(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<ItemDropGroupInfo[]>(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<ItemDropPackageInfo[]>(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; }
|
||
|
||
/// <summary>FR `ItemComposeTable.FindItemComposeJson` — 키 = 등급×1000 + 등급 단계.</summary>
|
||
public static ItemComposeJson FindItemComposeJson(eFRItemGrade grade, int gradeLv)
|
||
{
|
||
EnsureLoaded();
|
||
ItemComposeJson j;
|
||
return s_compose.TryGetValue((int)grade * 1000 + gradeLv, out j) ? j : null;
|
||
}
|
||
|
||
/// <summary>강등용 역조회 — 결과(등급·단계)가 주어진 값인 합성 행.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>FR `ComposePopup.FindResultItemJson` — 같은 그룹에서 결과 등급·단계의 장비.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>표의 등급 옵션 중 지금 등급까지 열린 것(FR `PlayerManager.GetTotalAbilities` 규칙 — 등급 오름차순, 초과 시 중단).</summary>
|
||
public static void CollectOpenedOptions(ItemJson item, List<ItemOptionElement> 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<Drop> 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();
|
||
}
|
||
}
|
||
}
|