181 lines
9.3 KiB
C#
181 lines
9.3 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 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<WLBoxTierWeight> tierWeights;
|
|
public int epicEvery; public int epicTier = 4;
|
|
public int sPity; public int sTier = 5;
|
|
}
|
|
[System.Serializable] public class WLBoxTableJson { public List<WLBoxDef> boxes; }
|
|
|
|
/// <summary>뽑기 1회 결과(연출용 · #825). json = 지급된 아이템 정의 · why = "(확정)"/"(천장)".</summary>
|
|
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; }
|
|
/// <summary>마지막 Draw 의 결과 목록(지급 순) — 결과 화면이 읽는다.</summary>
|
|
public static readonly List<WLDrawResult> LastResults = new List<WLDrawResult>();
|
|
|
|
static readonly Dictionary<int, WLBoxDef> s_boxes = new Dictionary<int, WLBoxDef>();
|
|
static readonly List<int> s_groups = new List<int>();
|
|
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<TextAsset>(path);
|
|
if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + path + ".json 없음");
|
|
var t = JsonConvert.DeserializeObject<WLBoxTableJson>(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<int> EquipGroups()
|
|
{
|
|
if (s_groups.Count > 0) return s_groups;
|
|
var items = WLEquipTables.Items;
|
|
var seen = new HashSet<int>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>상자를 count 번 연다. 열쇠가 모자라면 거기서 멈춘다. 돌려주는 값 = 실제 지급 수.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>UI 한 줄 — 열쇠 · 누적 · 다음 에픽 확정까지 · S급 확정까지.</summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>확률 표시(공정성 표준) — 「일반 55% · 고급 25% …」.</summary>
|
|
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();
|
|
}
|
|
}
|
|
}
|