// ───────────────────────────────────────────────────────────────────────────── // WLEquipGacha.cs — 장비 상자(열쇠) 더미 구현 (PD 2026-10-09 · #823) // // 표 = `Resources/WL/WLEquipBoxTable.json`(WLEquipSettings.boxTablePath · C45) // tierWeights : 등급별 출현 가중치(제안값) · epicEvery : N회마다 에픽 이상 확정(누적 개봉 수 기준) // sPity : 마지막 S급(전설) 뒤 N회째에 S급 확정(천장) · keyCost : 1회 열쇠 수(가격은 D3 보류) // 열쇠·누적 개봉·천장 카운터는 WLEquipSaveData(로컬 세이브)에 둔다. 지급 아이템 = 무작위 장비 그룹의 해당 등급 기본 단계. // ───────────────────────────────────────────────────────────────────────────── using System.Collections.Generic; using Newtonsoft.Json; using UnityEngine; namespace WL.Equip { [System.Serializable] public class WLBoxTierWeight { public int tier; public int weight; } [System.Serializable] public class WLBoxDef { public int id; public string name; public int keyCost = 1; public List tierWeights; public int epicEvery; public int epicTier = 4; public int sPity; public int sTier = 5; } [System.Serializable] public class WLBoxTableJson { public List boxes; } /// 뽑기 1회 결과(연출용 · #825). json = 지급된 아이템 정의 · why = "(확정)"/"(천장)". public struct WLDrawResult { public int tier; public bool isS; public string why; public ItemJson json; } public static class WLEquipGacha { public static bool Loaded { get; private set; } public static string LastLog { get; private set; } = ""; public static int Draws { get; private set; } public static int LastTier { get; private set; } /// 마지막 Draw 의 결과 목록(지급 순) — 결과 화면이 읽는다. public static readonly List LastResults = new List(); static readonly Dictionary s_boxes = new Dictionary(); static readonly List s_groups = new List(); static readonly System.Random s_rng = new System.Random(); public static bool EnsureLoaded() { if (Loaded) return true; var cfg = WLEquipSettings.Instance; string path = cfg != null && !string.IsNullOrEmpty(cfg.boxTablePath) ? cfg.boxTablePath : "WL/WLEquipBoxTable"; try { var ta = Resources.Load(path); if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + path + ".json 없음"); var t = JsonConvert.DeserializeObject(ta.text); s_boxes.Clear(); if (t.boxes != null) for (int i = 0; i < t.boxes.Count; i++) if (t.boxes[i] != null) s_boxes[t.boxes[i].id] = t.boxes[i]; Loaded = s_boxes.Count > 0; LastLog = "상자 표 로드 — 상자 " + s_boxes.Count; if (cfg != null && cfg.verboseLog != 0) Debug.Log("[WL-823] " + LastLog); } catch (System.Exception ex) { LastLog = "상자 표 로드 실패 — " + ex.Message; Debug.LogError("[WL-823] " + LastLog); } return Loaded; } public static WLBoxDef Box(int id) { EnsureLoaded(); WLBoxDef b; return s_boxes.TryGetValue(id, out b) ? b : null; } static List EquipGroups() { if (s_groups.Count > 0) return s_groups; var items = WLEquipTables.Items; var seen = new HashSet(); for (int i = 0; i < items.Count; i++) { var j = items[i]; if (!j.IsEquipment || j.GroupNo <= 0) continue; if (seen.Add(j.GroupNo)) s_groups.Add(j.GroupNo); } return s_groups; } /// 상자를 count 번 연다. 열쇠가 모자라면 거기서 멈춘다. 돌려주는 값 = 실제 지급 수. public static int Draw(int boxId, int count) { LastLog = ""; LastResults.Clear(); if (!WLInventory.EnsureLoaded() || !EnsureLoaded() || !WLEquipGrades.EnsureLoaded()) { LastLog = "표 로드 실패"; WLInventory.Commit(LastLog); return 0; } var box = Box(boxId); if (box == null) { LastLog = "상자 표 없음(" + boxId + ")"; WLInventory.Commit(LastLog); return 0; } var groups = EquipGroups(); if (groups.Count == 0) { LastLog = "장비 그룹 없음"; WLInventory.Commit(LastLog); return 0; } var d = WLInventory.Data; int done = 0; var sb = new System.Text.StringBuilder(); for (int i = 0; i < count; i++) { if (!WLInventory.TrySpendKeys(box.keyCost)) { sb.Append("열쇠 부족"); break; } string why; int tier = RollTier(box, d, out why); var step = WLEquipGrades.BaseStepOfTier(tier); int groupNo = groups[s_rng.Next(groups.Count)]; var json = step != null ? WLEquipTables.FindGroupItem(groupNo, (eFRItemGrade)step.frGrade, step.gradeLv) : null; d.boxDraws++; if (tier >= box.sTier) d.sinceS = 0; else d.sinceS++; LastTier = tier; if (json != null) { WLInventory.AddItem(json.Index, 1, 1); LastResults.Add(new WLDrawResult { tier = tier, isS = tier >= box.sTier, why = why, json = json }); done++; Draws++; if (sb.Length > 0) sb.Append(" · "); sb.Append(WLEquipGrades.TierLabel(tier, false)).Append(why).Append(' ').Append(json.Item_name); } else sb.Append(" [등급 ").Append(tier).Append(" 아이템 없음]"); } LastLog = sb.ToString(); WLInventory.Commit("상자 " + done + "회 — " + LastLog); return done; } static int RollTier(WLBoxDef b, WLEquipSaveData d, out string why) { why = ""; int tier = WeightedTier(b); if (b.sPity > 0 && d.sinceS + 1 >= b.sPity && tier < b.sTier) { tier = b.sTier; why = "(천장)"; } else if (b.epicEvery > 0 && (d.boxDraws + 1) % b.epicEvery == 0 && tier < b.epicTier) { tier = b.epicTier; why = "(확정)"; } return tier; } static int WeightedTier(WLBoxDef b) { if (b.tierWeights == null || b.tierWeights.Count == 0) return 1; int total = 0; for (int i = 0; i < b.tierWeights.Count; i++) total += Mathf.Max(0, b.tierWeights[i].weight); if (total <= 0) return b.tierWeights[0].tier; int r = s_rng.Next(total); for (int i = 0; i < b.tierWeights.Count; i++) { r -= Mathf.Max(0, b.tierWeights[i].weight); if (r < 0) return b.tierWeights[i].tier; } return b.tierWeights[b.tierWeights.Count - 1].tier; } /// UI 한 줄 — 열쇠 · 누적 · 다음 에픽 확정까지 · S급 확정까지. public static string InfoLine(int boxId) { if (!WLInventory.EnsureLoaded() || !EnsureLoaded()) return LastLog; var b = Box(boxId); var d = WLInventory.Data; if (b == null || d == null) return "상자 표 없음"; int toEpic = b.epicEvery > 0 ? b.epicEvery - (d.boxDraws % b.epicEvery) : 0; int toS = b.sPity > 0 ? Mathf.Max(0, b.sPity - d.sinceS) : 0; var sb = new System.Text.StringBuilder(); sb.Append(b.name).Append(" 열쇠 ").Append(d.keys).Append(" · 누적 ").Append(d.boxDraws).Append("회"); if (b.epicEvery > 0) sb.Append(" · ").Append(WLEquipGrades.TierLabel(b.epicTier, true)).Append(" 확정까지 ").Append(toEpic).Append("회"); if (b.sPity > 0) sb.Append(" · ").Append(WLEquipGrades.TierLabel(b.sTier, true)).Append(" 천장까지 ").Append(toS).Append("회"); return sb.ToString(); } /// 확률 표시(공정성 표준) — 「일반 55% · 고급 25% …」. public static string RateText(int boxId) { var b = Box(boxId); if (b == null || b.tierWeights == null) return ""; int total = 0; for (int i = 0; i < b.tierWeights.Count; i++) total += Mathf.Max(0, b.tierWeights[i].weight); if (total <= 0) return ""; var sb = new System.Text.StringBuilder(); for (int i = 0; i < b.tierWeights.Count; i++) { if (i > 0) sb.Append(" · "); sb.Append(WLEquipGrades.TierLabel(b.tierWeights[i].tier, true)).Append(' ').Append((100.0 * b.tierWeights[i].weight / total).ToString("F1")).Append('%'); } return sb.ToString(); } } }