Project_WL/Assets/WL/Equip/WLEquipGui.Gacha.cs

399 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WLEquipGui.Gacha.cs — 장비 상자(열쇠) 시트 · 결과 화면 · 확률/천장 팝업 (#825 연출을 v3 Layer Lab 스킨으로 이식 · 2026-10-12)
//
// 진입 = 허브 상단 열쇠 알약의 「+」. 바닥 시트(상자 흔들림 → 등급 색 번쩍임 → 결과 카드 등장 → 전설(S) 별 튀기) · 확률·천장 바.
// 로직은 WLEquipGacha(Draw · LastResults · Box). 치수·시간은 json `gacha`.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace WL.Equip
{
public partial class WLEquipGui
{
GameObject _boxPopup, _resultView, _ratesPopup;
RectTransform _chest, _resultGrid;
Image _chestImg, _chestGlow, _flash, _resultGlow, _epicBarFill, _sBarFill;
TextMeshProUGUI _boxInfo, _boxKeys, _boxLog, _resultTitle, _resultHint, _ratesPity;
Button _box1Btn, _box10Btn, _keyBtn, _ratesBtn, _againBtn, _resultCloseBtn;
readonly List<Cell> _resultCards = new List<Cell>();
readonly List<TextMeshProUGUI> _resultNames = new List<TextMeshProUGUI>();
readonly List<GameObject> _ratesRows = new List<GameObject>();
readonly List<Image> _ratesPills = new List<Image>();
readonly List<TextMeshProUGUI> _ratesNames = new List<TextMeshProUGUI>(), _ratesPcts = new List<TextMeshProUGUI>();
Coroutine _drawCo; bool _revealing, _skipReveal; int _lastDrawCount = 1;
public static int DrawsAnimated, ResultsShown;
// ── 시트 ──
void BuildBoxPopup()
{
_boxPopup = new GameObject("GachaSheet", typeof(RectTransform)); _boxPopup.layer = UILayer; _boxPopup.transform.SetParent(_root, false);
var prt = (RectTransform)_boxPopup.transform; Stretch(prt);
var dim = NewImage(prt, "Dim", null, Col("dim", new Color(0f, 0f, 0f, 0.7f)), Image.Type.Simple); Stretch(dim.rectTransform); dim.raycastTarget = true;
var dimBtn = dim.gameObject.AddComponent<Button>(); dimBtn.targetGraphic = dim; dimBtn.onClick.AddListener(CloseSheet);
var sheetFill = NewImage(prt, "SheetFill", Spr("innerBox"), Col("popupFill", new Color(0.14f, 0.11f, 0.17f, 0.98f))); sheetFill.raycastTarget = true;
Place(sheetFill.rectTransform, new Vector2(0.5f, 0f), new Vector2(0f, G.sheetBottom + 14f), new Vector2(G.sheetW - 28f, G.sheetH - 28f), new Vector2(0.5f, 0f));
var sheet = NewImage(prt, "Sheet", Spr("sheetBg"), Color.white);
var srt = sheet.rectTransform; srt.anchorMin = srt.anchorMax = new Vector2(0.5f, 0f); srt.pivot = new Vector2(0.5f, 0f);
srt.anchoredPosition = new Vector2(0f, G.sheetBottom); srt.sizeDelta = new Vector2(G.sheetW, G.sheetH);
var title = NewText(srt, "Title", G.titleFontPx, TextAlignmentOptions.Midline, TextCol()); title.text = "장비 상자"; title.fontStyle = FontStyles.Bold;
Place(title.rectTransform, new Vector2(0.5f, 1f), new Vector2(0f, -10f), new Vector2(G.sheetW - 240f, G.titleH - 20f), new Vector2(0.5f, 1f));
var close = NewIconButton(srt, "Close", "closeIcon", L.closeBtnW, CloseSheet, "closeBtnBg");
Place((RectTransform)close.transform, new Vector2(1f, 1f), new Vector2(-14f, -14f), new Vector2(L.closeBtnW, L.closeBtnH), new Vector2(1f, 1f));
_boxKeys = NewPill(srt, "Keys", "keyIcon", new Vector2(0f, 1f), new Vector2(G.edge + H.pillW + H.pillIcon * 0.3f, -G.titleH), false, null);
_keyBtn = NewButton(srt, "열쇠 +" + _cfg.cheatKeyGrant + " (테스트)", Spr("btnNeutral"), Color.white, G.cheatBtnW, G.cheatBtnH,
() => Act(() => { WLInventory.AddKeys(_cfg.cheatKeyGrant); WLInventory.Commit("열쇠 +" + _cfg.cheatKeyGrant); return true; }), G.infoFontPx - 6f, TextCol());
Place((RectTransform)_keyBtn.transform, new Vector2(1f, 1f), new Vector2(-G.edge, -G.titleH), new Vector2(G.cheatBtnW, G.cheatBtnH), new Vector2(1f, 1f));
_keyBtn.gameObject.SetActive(_cfg.cheatGrantEnabled != 0);
_chestGlow = NewImage(srt, "Glow", Spr("glow"), new Color(1f, 0.9f, 0.5f, 0.35f), Image.Type.Simple);
Place(_chestGlow.rectTransform, Vector2.zero, new Vector2(G.chestX, G.chestY), new Vector2(G.glowSize, G.glowSize), new Vector2(0.5f, 0.5f));
_chestImg = NewImage(srt, "Chest", Spr("chestIcon"), Color.white, Image.Type.Simple); _chestImg.preserveAspect = true;
Place(_chestImg.rectTransform, Vector2.zero, new Vector2(G.chestX, G.chestY), new Vector2(G.chestSize, G.chestSize), new Vector2(0.5f, 0.5f));
_chest = _chestImg.rectTransform;
_boxInfo = NewText(srt, "Info", G.infoFontPx, TextAlignmentOptions.TopLeft, TextCol()); _boxInfo.enableWordWrapping = true;
Place(_boxInfo.rectTransform, Vector2.zero, new Vector2(G.infoX, G.infoY), new Vector2(G.infoW, G.infoH), new Vector2(0f, 0f));
_boxLog = NewText(srt, "Log", G.infoFontPx - 6f, TextAlignmentOptions.BottomLeft, Col("textDim", Color.white)); _boxLog.enableWordWrapping = true;
Place(_boxLog.rectTransform, Vector2.zero, new Vector2(G.edge, G.drawBtnBottom + G.drawBtnH + 8f), new Vector2(G.sheetW - G.edge * 2f, G.logH), new Vector2(0f, 0f));
_ratesBtn = NewIconButton(srt, "Rates", "ratesIcon", G.ratesBtn, OpenRates);
Place((RectTransform)_ratesBtn.transform, new Vector2(1f, 0f), new Vector2(-G.edge, G.drawBtnBottom), new Vector2(G.ratesBtn, G.ratesBtn), new Vector2(1f, 0f));
_box1Btn = NewButton(srt, "1회", Spr("draw1Btn"), Color.white, G.drawBtnW, G.drawBtnH, () => StartDraw(1), G.drawFontPx, DarkCol());
Place((RectTransform)_box1Btn.transform, new Vector2(1f, 0f), new Vector2(-(G.edge + G.ratesBtn + G.drawGap), G.drawBtnBottom), new Vector2(G.drawBtnW, G.drawBtnH), new Vector2(1f, 0f));
_box10Btn = NewButton(srt, "10회", Spr("draw10Btn"), Color.white, G.drawBtnW, G.drawBtnH, () => StartDraw(10), G.drawFontPx, TextCol());
Place((RectTransform)_box10Btn.transform, new Vector2(1f, 0f), new Vector2(-(G.edge + G.ratesBtn + G.drawGap * 2f + G.drawBtnW), G.drawBtnBottom), new Vector2(G.drawBtnW, G.drawBtnH), new Vector2(1f, 0f));
_flash = NewImage(prt, "Flash", null, new Color(1f, 1f, 1f, 0f), Image.Type.Simple); Stretch(_flash.rectTransform);
_boxPopup.SetActive(false);
}
void OpenSheet() { if (_boxPopup == null) return; _popup.SetActive(false); _boxPopup.SetActive(true); if (_chestImg != null) _chestImg.sprite = Spr("chestIcon"); RefreshBox(); }
void CloseSheet() { if (_drawCo != null) return; _boxPopup.SetActive(false); }
void StartDraw(int count)
{
if (_drawCo != null || _revealing) return;
if (!isActiveAndEnabled) return; // 꺼진 화면에서는 코루틴을 시작할 수 없다(2026-10-12 「WL_EquipGui is inactive」 에러)
var bd = WLEquipGacha.Box(_cfg.defaultBoxId);
if (bd == null) { _boxLog.text = "상자 표 없음"; return; }
if (WLInventory.GetKeys() < bd.keyCost * count) { _boxLog.text = "열쇠가 부족합니다 (" + bd.keyCost * count + " 필요)"; return; }
_drawCo = StartCoroutine(Co_Draw(count, false));
}
/// <summary>상자 흔들림 → 뽑기 → 뚜껑 열림 + 등급 색 번쩍임 → 결과 화면. instant = 연출 생략(프로브).</summary>
IEnumerator Co_Draw(int count, bool instant)
{
_lastDrawCount = count;
SetEnabled(_box1Btn, false, "draw1Btn"); SetEnabled(_box10Btn, false, "draw10Btn");
if (!instant) yield return Co_Shake(_chest, G.shakeSec, G.shakeDeg);
int done = 0;
try { done = WLEquipGacha.Draw(_cfg.defaultBoxId, count); }
catch (System.Exception ex) { Debug.LogError("[WL-825] 뽑기 예외 — " + ex); }
RefreshBox();
if (done <= 0 || WLEquipGacha.LastResults.Count == 0) { _drawCo = null; RefreshBox(); yield break; }
int best = 1;
for (int i = 0; i < WLEquipGacha.LastResults.Count; i++) best = Mathf.Max(best, WLEquipGacha.LastResults[i].tier);
if (_chestImg != null && Spr("chestOpenIcon") != null) _chestImg.sprite = Spr("chestOpenIcon");
if (!instant) { DrawsAnimated++; yield return Co_Flash(WLEquipIcons.TierColor(best), G.flashSec); }
ShowResult(WLEquipGacha.LastResults, best, instant);
_drawCo = null;
RefreshBox();
}
IEnumerator Co_Shake(RectTransform rt, float sec, float deg)
{
if (rt == null || sec <= 0f) yield break;
float t = 0f;
while (t < sec)
{
t += Time.unscaledDeltaTime;
float k = Mathf.Clamp01(t / sec);
rt.localRotation = Quaternion.Euler(0f, 0f, Mathf.Sin(t * 45f) * deg * (1f - k * 0.5f));
rt.localScale = Vector3.one * (1f + 0.18f * Mathf.Sin(k * Mathf.PI));
yield return null;
}
rt.localRotation = Quaternion.identity; rt.localScale = Vector3.one;
}
IEnumerator Co_Flash(Color c, float sec)
{
if (_flash == null || sec <= 0f) yield break;
float t = 0f;
while (t < sec)
{
t += Time.unscaledDeltaTime;
float k = Mathf.Clamp01(t / sec);
float a = k < 0.3f ? k / 0.3f : 1f - (k - 0.3f) / 0.7f;
_flash.color = new Color(c.r, c.g, c.b, a * 0.9f);
yield return null;
}
_flash.color = new Color(c.r, c.g, c.b, 0f);
}
// ── 결과 화면 ──
void BuildResultView()
{
_resultView = new GameObject("GachaResult", typeof(RectTransform)); _resultView.layer = UILayer; _resultView.transform.SetParent(_root, false);
var prt = (RectTransform)_resultView.transform; Stretch(prt);
var dim = NewImage(prt, "Dim", null, new Color(0f, 0f, 0f, 0.88f), Image.Type.Simple); Stretch(dim.rectTransform); dim.raycastTarget = true;
var dimBtn = dim.gameObject.AddComponent<Button>(); dimBtn.targetGraphic = dim; dimBtn.onClick.AddListener(() => { if (_revealing) _skipReveal = true; });
_resultGlow = NewImage(prt, "Glow", Spr("glow"), new Color(1f, 0.85f, 0.4f, 0.45f), Image.Type.Simple);
Place(_resultGlow.rectTransform, new Vector2(0.5f, 0.5f), new Vector2(0f, G.resultGlowY), new Vector2(G.resultGlow, G.resultGlow));
_resultTitle = NewText(prt, "Title", G.resultTitleFontPx, TextAlignmentOptions.Midline, Color.white); _resultTitle.fontStyle = FontStyles.Bold;
_resultTitle.outlineWidth = 0.2f; _resultTitle.outlineColor = new Color32(40, 25, 10, 255);
Place(_resultTitle.rectTransform, new Vector2(0.5f, 1f), new Vector2(0f, -G.resultTitleTop), new Vector2(960f, G.resultTitleFontPx + 24f));
var cards = new GameObject("Cards", typeof(RectTransform), typeof(GridLayoutGroup), typeof(ContentSizeFitter)); cards.layer = UILayer; cards.transform.SetParent(prt, false);
_resultGrid = (RectTransform)cards.transform;
Place(_resultGrid, new Vector2(0.5f, 0.5f), new Vector2(0f, G.resultGridY), Vector2.zero);
var grid = cards.GetComponent<GridLayoutGroup>(); grid.spacing = new Vector2(G.resultGap, G.resultGap); grid.childAlignment = TextAnchor.MiddleCenter;
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount; grid.constraintCount = Mathf.Max(1, (int)G.resultCols);
var fit = cards.GetComponent<ContentSizeFitter>(); fit.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; fit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
_againBtn = NewButton(prt, "다시 뽑기", Spr("againBtn"), Color.white, G.againBtnW, G.againBtnH, () => { if (_revealing) return; HideResult(); StartDraw(_lastDrawCount); }, G.drawFontPx, DarkCol());
Place((RectTransform)_againBtn.transform, new Vector2(0.5f, 0f), new Vector2(0f, G.againY), new Vector2(G.againBtnW, G.againBtnH), new Vector2(0.5f, 0.5f));
_resultCloseBtn = NewButton(prt, "확인", Spr("closeTextBtn"), Color.white, G.closeBtnW, G.closeBtnH, () => { if (!_revealing) HideResult(); }, G.infoFontPx, TextCol());
Place((RectTransform)_resultCloseBtn.transform, new Vector2(0.5f, 0f), new Vector2(0f, G.closeY), new Vector2(G.closeBtnW, G.closeBtnH), new Vector2(0.5f, 0.5f));
_resultHint = NewText(prt, "Hint", G.resultNameFontPx, TextAlignmentOptions.Midline, new Color(1f, 1f, 1f, 0.75f)); _resultHint.text = "화면을 누르면 건너뛰기";
Place(_resultHint.rectTransform, new Vector2(0.5f, 0f), new Vector2(0f, G.closeY), new Vector2(700f, G.resultNameFontPx + 16f), new Vector2(0.5f, 0.5f));
_resultView.SetActive(false);
}
void ShowResult(List<WLDrawResult> res, int bestTier, bool instant)
{
if (_resultView == null) return;
ResultsShown++;
_resultView.SetActive(true);
int n = res.Count; bool big = n == 1;
float cs = big ? G.resultCardBig : G.resultCard;
var grid = _resultGrid.GetComponent<GridLayoutGroup>();
grid.cellSize = new Vector2(cs, cs + G.resultNameFontPx + 10f);
grid.constraintCount = big ? 1 : Mathf.Max(1, (int)G.resultCols);
for (int i = 0; i < n; i++)
{
if (i >= _resultCards.Count)
{
var holder = NewRT(_resultGrid, "Card" + i);
var c = NewCell(holder, "Cell", cs, L.cellIconInset, G.resultNameFontPx, false, false, null);
c.root.anchorMin = new Vector2(0f, 1f); c.root.anchorMax = new Vector2(1f, 1f); c.root.pivot = new Vector2(0.5f, 1f); c.root.anchoredPosition = Vector2.zero; c.root.sizeDelta = new Vector2(0f, cs);
var nm = NewText(holder, "Name", G.resultNameFontPx - 2f, TextAlignmentOptions.Top, TextCol());
nm.rectTransform.anchorMin = new Vector2(0f, 0f); nm.rectTransform.anchorMax = new Vector2(1f, 0f); nm.rectTransform.pivot = new Vector2(0.5f, 0f); nm.rectTransform.anchoredPosition = Vector2.zero; nm.rectTransform.sizeDelta = new Vector2(0f, G.resultNameFontPx + 6f);
nm.overflowMode = TextOverflowModes.Ellipsis;
_resultCards.Add(c); _resultNames.Add(nm);
}
var card = _resultCards[i];
var holderRt = (RectTransform)card.root.parent;
var le = holderRt.GetComponent<LayoutElement>() ?? holderRt.gameObject.AddComponent<LayoutElement>();
le.preferredWidth = cs; le.minWidth = cs; le.preferredHeight = cs + G.resultNameFontPx + 10f;
card.root.sizeDelta = new Vector2(0f, cs);
holderRt.gameObject.SetActive(true);
BindResultCard(card, _resultNames[i], res[i]);
holderRt.localScale = Vector3.zero;
}
for (int i = n; i < _resultCards.Count; i++) _resultCards[i].root.parent.gameObject.SetActive(false);
var bd = WLEquipGacha.Box(_cfg.defaultBoxId);
bool gotS = bd != null && bestTier >= bd.sTier;
var bc = WLEquipIcons.TierColor(bestTier);
_resultTitle.text = gotS ? "<color=#" + ColorUtility.ToHtmlStringRGB(bc) + ">" + WLEquipGrades.TierLabel(bestTier, false) + " 등장!</color>" : "획득!";
_resultGlow.color = new Color(bc.r, bc.g, bc.b, 0.45f);
DrawLabel(_againBtn, "다시 뽑기 ×" + n, (bd != null ? bd.keyCost : 1) * n);
_skipReveal = instant;
StartCoroutine(Co_Reveal(n));
}
void BindResultCard(Cell c, TextMeshProUGUI name, WLDrawResult r)
{
BindCellJson(c, r.json, 1);
c.level.text = WLEquipGrades.TierLabel(r.tier, false) + (string.IsNullOrEmpty(r.why) ? "" : " " + r.why);
c.lvBg.rectTransform.sizeDelta = new Vector2(c.size * (string.IsNullOrEmpty(r.why) ? 0.5f : 0.9f), c.lvBg.rectTransform.sizeDelta.y); // 「에픽 (확정)」 이 잘리지 않게
SetOn(c.lvBg, true);
if (name != null) name.text = r.json != null ? r.json.Item_name : "";
}
IEnumerator Co_Reveal(int n)
{
_revealing = true;
_againBtn.gameObject.SetActive(false); _resultCloseBtn.gameObject.SetActive(false); _resultHint.gameObject.SetActive(!_skipReveal);
yield return null;
for (int i = 0; i < n && i < _resultCards.Count; i++)
{
var holder = (RectTransform)_resultCards[i].root.parent;
if (_skipReveal) { holder.localScale = Vector3.one; continue; }
yield return Co_Pop(holder, G.popSec);
if (i < WLEquipGacha.LastResults.Count && WLEquipGacha.LastResults[i].isS) StartCoroutine(Co_Stars(holder));
float w = 0f; while (w < G.revealStagger && !_skipReveal) { w += Time.unscaledDeltaTime; yield return null; }
}
for (int i = 0; i < n && i < _resultCards.Count; i++) _resultCards[i].root.parent.localScale = Vector3.one;
_revealing = false; _skipReveal = false;
_resultHint.gameObject.SetActive(false);
var bd = WLEquipGacha.Box(_cfg.defaultBoxId); int kc = bd != null ? bd.keyCost : 1;
SetEnabled(_againBtn, WLInventory.GetKeys() >= kc * _lastDrawCount, "againBtn");
_againBtn.gameObject.SetActive(true); _resultCloseBtn.gameObject.SetActive(true);
}
IEnumerator Co_Pop(RectTransform rt, float sec)
{
float t = 0f;
while (t < sec && !_skipReveal)
{
t += Time.unscaledDeltaTime; float k = Mathf.Clamp01(t / sec);
float s = k < 0.6f ? Mathf.Lerp(0f, 1.15f, k / 0.6f) : Mathf.Lerp(1.15f, 1f, (k - 0.6f) / 0.4f);
rt.localScale = Vector3.one * s;
yield return null;
}
rt.localScale = Vector3.one;
}
/// <summary>S급(전설 이상) 카드 — 별이 사방으로 튀어 사라진다.</summary>
IEnumerator Co_Stars(RectTransform around)
{
int n = Mathf.Max(0, (int)G.starCount);
if (n == 0 || Spr("star") == null || around == null) yield break;
var stars = new List<Image>(n); var dirs = new List<Vector2>(n);
for (int i = 0; i < n; i++)
{
var img = NewImage(around, "Star", Spr("star"), new Color(1f, 0.95f, 0.5f, 1f), Image.Type.Simple); img.preserveAspect = true;
Place(img.rectTransform, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(G.starSize, G.starSize));
float a = (i + Random.value * 0.5f) / n * Mathf.PI * 2f;
stars.Add(img); dirs.Add(new Vector2(Mathf.Cos(a), Mathf.Sin(a)));
}
float t = 0f;
while (t < G.starSec)
{
t += Time.unscaledDeltaTime; float k = Mathf.Clamp01(t / G.starSec);
for (int i = 0; i < stars.Count; i++)
{
if (stars[i] == null) continue;
stars[i].rectTransform.anchoredPosition = dirs[i] * (G.starFly * Mathf.Sin(k * Mathf.PI * 0.5f));
stars[i].rectTransform.localScale = Vector3.one * (1f - k * 0.6f);
stars[i].color = new Color(1f, 0.95f, 0.5f, 1f - k);
}
yield return null;
}
for (int i = 0; i < stars.Count; i++) if (stars[i] != null) Destroy(stars[i].gameObject);
}
void HideResult()
{
if (_resultView != null) _resultView.SetActive(false);
_revealing = false; _skipReveal = false;
if (_chestImg != null) _chestImg.sprite = Spr("chestIcon");
RefreshBox();
}
// ── 확률·천장 팝업 ──
void BuildRatesPopup()
{
_ratesPopup = new GameObject("RatesPopup", typeof(RectTransform)); _ratesPopup.layer = UILayer; _ratesPopup.transform.SetParent(_root, false);
var prt = (RectTransform)_ratesPopup.transform; Stretch(prt);
var dim = NewImage(prt, "Dim", null, Col("dim", new Color(0f, 0f, 0f, 0.7f)), Image.Type.Simple); Stretch(dim.rectTransform); dim.raycastTarget = true;
var dimBtn = dim.gameObject.AddComponent<Button>(); dimBtn.targetGraphic = dim; dimBtn.onClick.AddListener(() => _ratesPopup.SetActive(false));
var box = NewPopupBox(prt, "Box", new Vector2(G.ratesW, G.ratesH));
var brt = box.rectTransform;
var t = NewText(brt, "Title", L.popupNameFontPx, TextAlignmentOptions.Midline, TextCol()); t.text = "확률 · 천장"; t.fontStyle = FontStyles.Bold;
Place(t.rectTransform, new Vector2(0.5f, 1f), new Vector2(0f, -16f), new Vector2(G.ratesW - 240f, L.popupTitleH - 30f), new Vector2(0.5f, 1f));
var closeBtn = NewIconButton(brt, "Close", "closeIcon", L.closeBtnW, () => _ratesPopup.SetActive(false), "closeBtnBg");
Place((RectTransform)closeBtn.transform, new Vector2(1f, 1f), new Vector2(-14f, -14f), new Vector2(L.closeBtnW, L.closeBtnH), new Vector2(1f, 1f));
var body = NewRT(brt, "Body");
body.anchorMin = new Vector2(0f, 0f); body.anchorMax = new Vector2(1f, 1f); body.offsetMin = new Vector2(40f, 40f); body.offsetMax = new Vector2(-40f, -L.popupTitleH - 10f);
var vl = body.gameObject.AddComponent<VerticalLayoutGroup>();
vl.padding = new RectOffset(0, 0, 0, 0); vl.spacing = 10f; vl.childAlignment = TextAnchor.UpperCenter;
vl.childControlWidth = true; vl.childControlHeight = true; vl.childForceExpandWidth = true; vl.childForceExpandHeight = false;
for (int i = 0; i < 7; i++)
{
var row = NewRow(body, "Rate" + i, G.ratesRowH, 14f);
var hl = row.GetComponent<HorizontalLayoutGroup>(); hl.childAlignment = TextAnchor.MiddleLeft;
var pill = NewImage(row, "Pill", Spr("gradeLabel"), Color.white); Pref(pill.rectTransform, G.ratesPillW, G.ratesRowH - 16f);
var pl = NewText(pill.rectTransform, "Label", G.ratesFontPx - 4f, TextAlignmentOptions.Midline, Color.white); pl.fontStyle = FontStyles.Bold; Stretch(pl.rectTransform);
var name = NewText(row, "Name", G.ratesFontPx, TextAlignmentOptions.MidlineLeft, TextCol()); Flex(name.rectTransform, 1f);
var pct = NewText(row, "Pct", G.ratesFontPx, TextAlignmentOptions.MidlineRight, TextCol()); pct.fontStyle = FontStyles.Bold; Pref(pct.rectTransform, 200f, G.ratesRowH);
_ratesRows.Add(row.gameObject); _ratesPills.Add(pill); _ratesNames.Add(name); _ratesPcts.Add(pct);
}
_ratesPity = NewText(body, "Pity", G.ratesFontPx, TextAlignmentOptions.TopLeft, TextCol()); _ratesPity.enableWordWrapping = true; Pref(_ratesPity.rectTransform, 0f, G.ratesFontPx * 2f + 28f);
_epicBarFill = NewBar(body, "EpicBar");
_sBarFill = NewBar(body, "SBar");
var note = NewText(body, "Note", G.ratesFontPx - 6f, TextAlignmentOptions.TopLeft, Col("textDim", Color.white)); note.enableWordWrapping = true;
note.text = "확률·확정 주기·천장은 제안값(PD 확정 전) · 10회마다 에픽 이상 확정 · 전설 천장은 자연 전설이 나오면 다시 셉니다 · 불멸·신화는 합성 승급으로만";
var nle = note.gameObject.AddComponent<LayoutElement>(); nle.flexibleHeight = 1f; nle.minHeight = 80f;
_ratesPopup.SetActive(false);
}
Image NewBar(RectTransform parent, string name)
{
var bg = NewImage(parent, name, Spr("barBg"), Spr("barBg") != null ? Color.white : new Color(0f, 0f, 0f, 0.25f)); Pref(bg.rectTransform, 0f, G.barH);
var fill = NewImage(bg.rectTransform, "Fill", Spr("barFill"), new Color(1f, 0.75f, 0.2f, 1f));
fill.rectTransform.anchorMin = Vector2.zero; fill.rectTransform.anchorMax = new Vector2(0f, 1f); fill.rectTransform.pivot = new Vector2(0f, 0.5f);
fill.rectTransform.offsetMin = new Vector2(4f, 4f); fill.rectTransform.offsetMax = new Vector2(-4f, -4f);
return fill;
}
static void SetBar(Image fill, float ratio)
{
if (fill == null) return;
ratio = Mathf.Clamp01(ratio);
fill.gameObject.SetActive(ratio > 0.02f);
fill.rectTransform.anchorMax = new Vector2(ratio, 1f);
}
void OpenRates() { if (_ratesPopup == null) return; _ratesPopup.SetActive(true); RefreshRates(); }
void RefreshRates()
{
var bd = WLEquipGacha.Box(_cfg.defaultBoxId); var d = WLInventory.Data;
int shown = 0;
if (bd != null && bd.tierWeights != null)
{
int total = 0; for (int i = 0; i < bd.tierWeights.Count; i++) total += Mathf.Max(0, bd.tierWeights[i].weight);
for (int i = 0; i < bd.tierWeights.Count && shown < _ratesRows.Count; i++)
{
var tw = bd.tierWeights[i];
_ratesRows[shown].SetActive(true);
_ratesPills[shown].color = TierCol(tw.tier);
var pl = _ratesPills[shown].GetComponentInChildren<TextMeshProUGUI>(); if (pl != null) pl.text = WLEquipGrades.TierLabel(tw.tier, false);
_ratesNames[shown].text = WLEquipGrades.TierLabel(tw.tier, false) + (tw.tier >= bd.sTier ? " <size=75%>(S급)</size>" : "");
_ratesPcts[shown].text = total > 0 ? (100.0 * tw.weight / total).ToString("F1") + "%" : "-";
shown++;
}
}
for (int i = shown; i < _ratesRows.Count; i++) _ratesRows[i].SetActive(false);
if (bd == null || d == null) { _ratesPity.text = "상자 표 없음"; return; }
int toEpic = bd.epicEvery > 0 ? bd.epicEvery - (d.boxDraws % bd.epicEvery) : 0;
int toS = bd.sPity > 0 ? Mathf.Max(0, bd.sPity - d.sinceS) : 0;
_ratesPity.text = (bd.epicEvery > 0 ? WLEquipGrades.TierLabel(bd.epicTier, false) + " 확정까지 " + toEpic + "회 (" + bd.epicEvery + "회마다)\n" : "")
+ (bd.sPity > 0 ? WLEquipGrades.TierLabel(bd.sTier, false) + " 천장까지 " + toS + "회 (" + bd.sPity + "회)" : "");
SetBar(_epicBarFill, bd.epicEvery > 0 ? 1f - (float)toEpic / bd.epicEvery : 0f);
SetBar(_sBarFill, bd.sPity > 0 ? 1f - (float)toS / bd.sPity : 0f);
}
void DrawLabel(Button b, string head, int keys) { if (b != null) SetButtonLabel(b, head + "\n<size=70%>열쇠 " + keys + "</size>"); }
void RefreshBox()
{
if (_boxPopup == null || !_boxPopup.activeSelf) return;
var bd = WLEquipGacha.Box(_cfg.defaultBoxId); int kc = bd != null ? bd.keyCost : 1; int keys = WLInventory.GetKeys();
_boxKeys.text = keys.ToString();
_boxInfo.text = WLEquipGacha.InfoLine(_cfg.defaultBoxId).Replace(" · ", "\n");
_boxLog.text = string.IsNullOrEmpty(WLEquipGacha.LastLog) ? "" : "최근: " + WLEquipGacha.LastLog;
DrawLabel(_box1Btn, "1회", kc); DrawLabel(_box10Btn, "10회", kc * 10);
bool busy = _drawCo != null;
SetEnabled(_box1Btn, !busy && bd != null && keys >= kc, "draw1Btn");
SetEnabled(_box10Btn, !busy && bd != null && keys >= kc * 10, "draw10Btn");
if (_ratesPopup != null && _ratesPopup.activeSelf) RefreshRates();
}
// 프로브용
public void ShowBoxForProbe() { if (!gameObject.activeSelf) Open(); OpenSheet(); }
/// <summary>프로브용 뽑기 — 화면이 닫혀 있으면 먼저 연다(닫힌 GameObject 에서 StartCoroutine 은 에러).</summary>
public void DrawForProbe(int count, bool instant)
{
if (!gameObject.activeSelf) { Open(); OpenSheet(); }
if (!isActiveAndEnabled) return;
if (_drawCo == null && !_revealing) _drawCo = StartCoroutine(Co_Draw(count, instant));
}
public void ShowRatesForProbe() { OpenRates(); }
public void HideResultForProbe() { HideResult(); }
public bool IsBusyForProbe { get { return _drawCo != null || _revealing; } }
public bool ResultVisibleForProbe { get { return _resultView != null && _resultView.activeSelf; } }
public bool SheetVisibleForProbe { get { return _boxPopup != null && _boxPopup.activeSelf; } }
}
}