497 lines
30 KiB
C#
497 lines
30 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLEquipUi.cs — FR 장비 시스템 더미 UI (#821 · PD 「더미 UI 라도 좋으니 실제 기능 동작」)
|
||
//
|
||
// 코드로 만드는 uGUI(프리팹 0 · 스프라이트 0 · 글자만). 자기 Canvas(Overlay · 1080×1920 기준)를 들고
|
||
// 하단 메뉴 DummyMenuButton(menuId = weapon/upgrade · WLEquipSettings.openMenuIds)에서 연다.
|
||
// 화면: 장착 슬롯 6 → 보유 목록(스크롤) → 선택 아이템 상세 + 버튼(장착/해제 · 강화 · 전부 강화 · 합성 · 전부 합성 · 강등 · 테스트 획득 · 초기화).
|
||
// 정식 UI 는 PD 기획 후 교체 — 로직(WLInventory)은 그대로 쓴다.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System.Collections.Generic;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.UI;
|
||
|
||
namespace WL.Equip
|
||
{
|
||
public class WLEquipUi : MonoBehaviour
|
||
{
|
||
public static WLEquipUi Instance { get; private set; }
|
||
public static bool IsOpen { get { return (Instance != null && Instance.gameObject.activeSelf) || WLEquipGui.IsOpen; } }
|
||
public static int Opens { get; private set; }
|
||
|
||
const int UILayer = 5;
|
||
|
||
WLEquipSettings _cfg;
|
||
TMP_FontAsset _font;
|
||
Canvas _canvas;
|
||
TextMeshProUGUI _title, _info, _stats, _boxInfo, _detail, _message, _gains;
|
||
readonly Button[] _slotBtn = new Button[6];
|
||
readonly TextMeshProUGUI[] _slotLabel = new TextMeshProUGUI[6];
|
||
RectTransform _listContent;
|
||
ScrollRect _scroll;
|
||
readonly List<Button> _rows = new List<Button>();
|
||
readonly List<TextMeshProUGUI> _rowLabels = new List<TextMeshProUGUI>();
|
||
readonly List<Image> _rowBg = new List<Image>();
|
||
readonly List<int> _rowIds = new List<int>();
|
||
Button _equipBtn, _lvBtn, _lvAllBtn, _composeBtn, _composeAllBtn, _downBtn, _cheatBtn, _resetBtn, _boxBtn, _box10Btn, _keyBtn;
|
||
TextMeshProUGUI _equipLabel;
|
||
int _selectedId = -1;
|
||
|
||
// ── 진입점 ───────────────────────────────────────────────────────
|
||
/// <summary>DummyMenuButton 1줄 훅 — 이 menuId 가 장비 메뉴면 열고 true.</summary>
|
||
public static bool TryOpen(string menuId)
|
||
{
|
||
var cfg = WLEquipSettings.Instance;
|
||
if (cfg == null || cfg.enabled_ == 0 || !cfg.IsOpenMenuId(menuId)) return false;
|
||
Open();
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 🔴 하단 「무기」 버튼(HUD_BottomMenu.prefab)에는 힘민지 원본 `NewGameUI.OnClick_Button`(원본 EquipmentUI 열기)이
|
||
/// 프리팹에 연결돼 있고 persistent 호출이 런타임 리스너보다 먼저 돈다 → 원본 메뉴가 초기화 없이 켜지면
|
||
/// `FollowTextEnd` 가 매 프레임 NRE 를 낸다(2026-10-08 실측 · 500건). 장비 화면을 열 때 그 메뉴가 켜져 있으면 끈다.
|
||
/// </summary>
|
||
public static bool CloseLegacyEquipmentUi()
|
||
{
|
||
var cfg = WLEquipSettings.Instance;
|
||
if (cfg == null || cfg.closeLegacyEquipmentUi == 0) return false;
|
||
try
|
||
{
|
||
var ng = NewGameUI.Ins;
|
||
if (ng != null && ng.m_EquipmentUI != null && ng.m_EquipmentUI.gameObject.activeSelf)
|
||
{
|
||
ng.m_EquipmentUI.gameObject.SetActive(false);
|
||
return true;
|
||
}
|
||
}
|
||
catch (System.Exception ex) { Debug.LogWarning("[WL-821] 원본 EquipmentUI 닫기 실패 — " + ex.Message); }
|
||
return false;
|
||
}
|
||
|
||
public static WLEquipUi Open()
|
||
{
|
||
var cfg = WLEquipSettings.Instance;
|
||
if (cfg == null || cfg.enabled_ == 0) return null;
|
||
CloseLegacyEquipmentUi();
|
||
if (cfg.useGuiV2 != 0) { WLEquipGui.Open(); return null; } // #824 XP Hero 레이아웃 GUI
|
||
if (Instance == null)
|
||
{
|
||
var go = new GameObject("WL_EquipUi", typeof(RectTransform));
|
||
go.layer = UILayer;
|
||
DontDestroyOnLoad(go);
|
||
Instance = go.AddComponent<WLEquipUi>();
|
||
Instance._cfg = cfg;
|
||
Instance.Build();
|
||
}
|
||
Instance.gameObject.SetActive(true);
|
||
Opens++;
|
||
WLInventory.EnsureLoaded();
|
||
Instance.Refresh();
|
||
return Instance;
|
||
}
|
||
|
||
public static void Close() { if (Instance != null) Instance.gameObject.SetActive(false); }
|
||
|
||
void OnEnable() { WLInventory.Changed += Refresh; }
|
||
void OnDisable() { WLInventory.Changed -= Refresh; }
|
||
void OnDestroy() { if (Instance == this) Instance = null; }
|
||
|
||
// ── 조립 ─────────────────────────────────────────────────────────
|
||
void Build()
|
||
{
|
||
_font = ResolveFont();
|
||
if (EventSystem.current == null)
|
||
{
|
||
var es = new GameObject("WL_EventSystem(Equip)", typeof(EventSystem), typeof(StandaloneInputModule));
|
||
DontDestroyOnLoad(es);
|
||
}
|
||
|
||
_canvas = gameObject.AddComponent<Canvas>();
|
||
_canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||
_canvas.sortingOrder = _cfg.uiSortingOrder;
|
||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||
scaler.referenceResolution = new Vector2(1080f, 1920f);
|
||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||
scaler.matchWidthOrHeight = 0.5f;
|
||
gameObject.AddComponent<GraphicRaycaster>();
|
||
|
||
var root = (RectTransform)transform;
|
||
var dim = NewImage(root, "Dim", _cfg.dimColor); Stretch(dim.rectTransform); dim.raycastTarget = true;
|
||
|
||
var panel = NewImage(root, "Panel", _cfg.panelColor);
|
||
Stretch(panel.rectTransform);
|
||
float m = _cfg.panelMarginPx;
|
||
panel.rectTransform.offsetMin = new Vector2(m, m); panel.rectTransform.offsetMax = new Vector2(-m, -m);
|
||
var vl = panel.gameObject.AddComponent<VerticalLayoutGroup>();
|
||
vl.padding = new RectOffset(16, 16, 16, 16); vl.spacing = 10f;
|
||
vl.childControlWidth = true; vl.childControlHeight = true; vl.childForceExpandWidth = true; vl.childForceExpandHeight = false;
|
||
var prt = panel.rectTransform;
|
||
|
||
// 제목 + 닫기
|
||
var titleRow = NewRow(prt, "TitleRow", 70f);
|
||
_title = NewText(titleRow, "Title", _cfg.titleFontPx, TextAlignmentOptions.MidlineLeft); Flex(_title.rectTransform, 1f);
|
||
NewButton(titleRow, "닫기", 160f, Close);
|
||
|
||
_info = NewText(prt, "Info", _cfg.smallFontPx, TextAlignmentOptions.MidlineLeft); Pref(_info.rectTransform, 44f);
|
||
_stats = NewText(prt, "Stats", _cfg.smallFontPx, TextAlignmentOptions.MidlineLeft); Pref(_stats.rectTransform, 44f);
|
||
_stats.color = new Color(1f, 0.92f, 0.6f, 1f);
|
||
|
||
// 장착 슬롯 2×3
|
||
for (int r = 0; r < 2; r++)
|
||
{
|
||
var row = NewRow(prt, "SlotRow" + r, _cfg.slotHeightPx);
|
||
for (int c = 0; c < 3; c++)
|
||
{
|
||
int slotIndex = r * 3 + c;
|
||
_slotBtn[slotIndex] = NewButton(row, "", 0f, () => OnSlotClick(slotIndex));
|
||
Flex((RectTransform)_slotBtn[slotIndex].transform, 1f);
|
||
_slotLabel[slotIndex] = _slotBtn[slotIndex].GetComponentInChildren<TextMeshProUGUI>();
|
||
_slotLabel[slotIndex].fontSize = _cfg.smallFontPx;
|
||
}
|
||
}
|
||
|
||
// 장비 상자(PD 2026-10-09 — 10회마다 에픽 확정 · 50회 S급 천장 · 열쇠 가격은 보류)
|
||
_boxInfo = NewText(prt, "BoxInfo", _cfg.smallFontPx, TextAlignmentOptions.MidlineLeft); Pref(_boxInfo.rectTransform, 44f);
|
||
var boxRow = NewRow(prt, "BoxRow", _cfg.buttonHeightPx);
|
||
var boxDef = WLEquipGacha.Box(_cfg.defaultBoxId);
|
||
int keyCost = boxDef != null ? boxDef.keyCost : 1;
|
||
_boxBtn = NewButton(boxRow, "상자 1회 (열쇠 " + keyCost + ")", 0f, () => Act(() => WLEquipGacha.Draw(_cfg.defaultBoxId, 1) > 0)); Flex((RectTransform)_boxBtn.transform, 1f);
|
||
_box10Btn = NewButton(boxRow, "상자 10회 (열쇠 " + (keyCost * 10) + ")", 0f, () => Act(() => WLEquipGacha.Draw(_cfg.defaultBoxId, 10) > 0)); Flex((RectTransform)_box10Btn.transform, 1f);
|
||
_keyBtn = NewButton(boxRow, "열쇠 +" + _cfg.cheatKeyGrant, 0f, () => Act(() => { WLInventory.AddKeys(_cfg.cheatKeyGrant); WLInventory.Commit("열쇠 +" + _cfg.cheatKeyGrant); return true; })); Flex((RectTransform)_keyBtn.transform, 1f);
|
||
_keyBtn.gameObject.SetActive(_cfg.cheatGrantEnabled != 0);
|
||
|
||
// 목록(스크롤)
|
||
var listGo = new GameObject("List", typeof(RectTransform), typeof(Image), typeof(ScrollRect));
|
||
listGo.layer = UILayer; listGo.transform.SetParent(prt, false);
|
||
var listImg = listGo.GetComponent<Image>(); listImg.color = _cfg.boxColor;
|
||
var le = listGo.AddComponent<LayoutElement>(); le.flexibleHeight = 1f; le.minHeight = 200f;
|
||
_scroll = listGo.GetComponent<ScrollRect>();
|
||
var viewport = new GameObject("Viewport", typeof(RectTransform), typeof(Image), typeof(RectMask2D));
|
||
viewport.layer = UILayer; viewport.transform.SetParent(listGo.transform, false);
|
||
var vrt = (RectTransform)viewport.transform; Stretch(vrt);
|
||
viewport.GetComponent<Image>().color = new Color(0f, 0f, 0f, 0.01f);
|
||
var content = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
|
||
content.layer = UILayer; content.transform.SetParent(viewport.transform, false);
|
||
_listContent = (RectTransform)content.transform;
|
||
_listContent.anchorMin = new Vector2(0f, 1f); _listContent.anchorMax = new Vector2(1f, 1f); _listContent.pivot = new Vector2(0.5f, 1f);
|
||
_listContent.offsetMin = Vector2.zero; _listContent.offsetMax = Vector2.zero;
|
||
var cvl = content.GetComponent<VerticalLayoutGroup>();
|
||
cvl.padding = new RectOffset(8, 8, 8, 8); cvl.spacing = 6f;
|
||
cvl.childControlWidth = true; cvl.childControlHeight = true; cvl.childForceExpandWidth = true; cvl.childForceExpandHeight = false;
|
||
content.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||
_scroll.content = _listContent; _scroll.viewport = vrt; _scroll.horizontal = false; _scroll.vertical = true;
|
||
_scroll.movementType = ScrollRect.MovementType.Clamped; _scroll.scrollSensitivity = 40f;
|
||
|
||
// 상세 + 버튼
|
||
var detailBox = NewImage(prt, "Detail", _cfg.boxColor); Pref(detailBox.rectTransform, _cfg.detailHeightPx);
|
||
var dvl = detailBox.gameObject.AddComponent<VerticalLayoutGroup>();
|
||
dvl.padding = new RectOffset(12, 12, 10, 10); dvl.spacing = 8f;
|
||
dvl.childControlWidth = true; dvl.childControlHeight = true; dvl.childForceExpandWidth = true; dvl.childForceExpandHeight = false;
|
||
_detail = NewText(detailBox.rectTransform, "Text", _cfg.smallFontPx, TextAlignmentOptions.TopLeft); Flex(_detail.rectTransform, 1f);
|
||
var b1 = NewRow(detailBox.rectTransform, "Buttons1", _cfg.buttonHeightPx);
|
||
_equipBtn = NewButton(b1, "장착", 0f, OnEquipToggle); Flex((RectTransform)_equipBtn.transform, 1f);
|
||
_equipLabel = _equipBtn.GetComponentInChildren<TextMeshProUGUI>();
|
||
_lvBtn = NewButton(b1, "강화 +1", 0f, () => Act(() => WLInventory.ItemLevelUp(_selectedId, false))); Flex((RectTransform)_lvBtn.transform, 1f);
|
||
_lvAllBtn = NewButton(b1, "전부 강화", 0f, () => Act(() => WLInventory.ItemLevelUp(_selectedId, true))); Flex((RectTransform)_lvAllBtn.transform, 1f);
|
||
_composeBtn = NewButton(b1, "합성", 0f, () => Act(() => WLInventory.ComposeOne(_selectedId))); Flex((RectTransform)_composeBtn.transform, 1f);
|
||
var b2 = NewRow(detailBox.rectTransform, "Buttons2", _cfg.buttonHeightPx);
|
||
_composeAllBtn = NewButton(b2, "전부 합성", 0f, () => Act(() => WLInventory.ComposeAll() > 0)); Flex((RectTransform)_composeAllBtn.transform, 1f);
|
||
_downBtn = NewButton(b2, "강등", 0f, () => Act(() => WLInventory.Downgrade(_selectedId))); Flex((RectTransform)_downBtn.transform, 1f);
|
||
_cheatBtn = NewButton(b2, "테스트 획득", 0f, OnCheat); Flex((RectTransform)_cheatBtn.transform, 1f);
|
||
_cheatBtn.gameObject.SetActive(_cfg.cheatGrantEnabled != 0);
|
||
_resetBtn = NewButton(b2, "초기화", 0f, () => { WLInventory.ResetAll(); _selectedId = -1; Refresh(); }); Flex((RectTransform)_resetBtn.transform, 1f);
|
||
|
||
_message = NewText(prt, "Message", _cfg.smallFontPx, TextAlignmentOptions.MidlineLeft); Pref(_message.rectTransform, 40f);
|
||
_gains = NewText(prt, "Gains", _cfg.smallFontPx, TextAlignmentOptions.MidlineLeft); Pref(_gains.rectTransform, 40f);
|
||
_gains.color = new Color(0.8f, 0.9f, 0.6f, 1f);
|
||
}
|
||
|
||
TMP_FontAsset ResolveFont() { return ResolveKoreanFont(_cfg); }
|
||
|
||
/// <summary>한글 글리프('장'·'비')를 가진 TMP 폰트를 고른다 — 아무 라벨의 폰트를 빌리면 FI 데모 폰트(한글 없음)가 걸려 □ 로 나온다(2026-10-08 캡처 실측).</summary>
|
||
public static TMP_FontAsset ResolveKoreanFont(WLEquipSettings cfg)
|
||
{
|
||
TMP_FontAsset f = null;
|
||
if (cfg != null && !string.IsNullOrEmpty(cfg.fontPath)) f = WL.Island.WLIslandAssets.Font(cfg.fontPath);
|
||
if (f != null && HasKorean(f)) return f;
|
||
var labels = FindObjectsByType<TextMeshProUGUI>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
TMP_FontAsset fallback = null;
|
||
for (int i = 0; i < labels.Length; i++)
|
||
{
|
||
var lf = labels[i] != null ? labels[i].font : null;
|
||
if (lf == null) continue;
|
||
if (fallback == null) fallback = lf;
|
||
if (HasKorean(lf)) return lf;
|
||
}
|
||
return f != null ? f : fallback;
|
||
}
|
||
|
||
static bool HasKorean(TMP_FontAsset f)
|
||
{
|
||
try { return f.HasCharacter('장', true, true) && f.HasCharacter('비', true, true); }
|
||
catch { return false; }
|
||
}
|
||
|
||
// ── 갱신 ─────────────────────────────────────────────────────────
|
||
public void Refresh()
|
||
{
|
||
if (!gameObject.activeSelf) return;
|
||
if (!WLInventory.EnsureLoaded()) { _title.text = "장비 — 표 로드 실패"; _detail.text = WLEquipTables.LastLog; return; }
|
||
_title.text = "장비 (등급 7단계 · 더미 UI) 보유 " + WLInventory.ItemCount;
|
||
|
||
var sb = new System.Text.StringBuilder();
|
||
sb.Append("코인 ").Append(WLInventory.GetGold()).Append(" 강화석:");
|
||
if (_cfg.clearStoneItemIds != null)
|
||
for (int i = 0; i < _cfg.clearStoneItemIds.Length; i++)
|
||
{
|
||
var sj = WLEquipTables.FindItemJson(_cfg.clearStoneItemIds[i]);
|
||
sb.Append(' ').Append(sj != null ? sj.Item_name.Replace(" 강화석", "") : _cfg.clearStoneItemIds[i].ToString()).Append(' ').Append(WLInventory.GetItemCountByIndex(_cfg.clearStoneItemIds[i]));
|
||
}
|
||
_info.text = sb.ToString();
|
||
_stats.text = WLEquipStats.StatusLine();
|
||
_boxInfo.text = WLEquipGacha.InfoLine(_cfg.defaultBoxId);
|
||
var boxDef = WLEquipGacha.Box(_cfg.defaultBoxId);
|
||
int keyCost = boxDef != null ? boxDef.keyCost : 1;
|
||
int keys = WLInventory.GetKeys();
|
||
_boxBtn.interactable = boxDef != null && keys >= keyCost;
|
||
_box10Btn.interactable = boxDef != null && keys >= keyCost * 10;
|
||
SetButtonColor(_boxBtn, _boxBtn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||
SetButtonColor(_box10Btn, _box10Btn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||
|
||
for (int i = 0; i < 6; i++)
|
||
{
|
||
var slot = (eFREquipmentType)(i + 1);
|
||
var it = WLInventory.EquippedInSlot(slot);
|
||
_slotLabel[i].text = WLEquipTables.SlotName(slot) + "\n" + (it != null
|
||
? it.itemJson.Item_name + " [" + WLEquipGrades.Label(it.itemJson, true) + "] Lv" + it.rawItem.level
|
||
: "(비어 있음)");
|
||
SetButtonColor(_slotBtn[i], it != null && it.rawItem._id == _selectedId ? _cfg.selectedRowColor : (it != null ? _cfg.equippedRowColor : _cfg.buttonDisabledColor));
|
||
}
|
||
|
||
var items = WLInventory.SortedItems();
|
||
int n = Mathf.Min(items.Count, _cfg.maxListRows);
|
||
while (_rows.Count < n) AddRow();
|
||
for (int i = 0; i < _rows.Count; i++)
|
||
{
|
||
bool on = i < n;
|
||
_rows[i].gameObject.SetActive(on);
|
||
if (!on) continue;
|
||
var it = items[i];
|
||
_rowIds[i] = it.rawItem._id;
|
||
_rowLabels[i].text = WLInventory.RowText(it);
|
||
_rowBg[i].color = it.rawItem._id == _selectedId ? _cfg.selectedRowColor : (WLInventory.IsEquipped(it.rawItem._id) ? _cfg.equippedRowColor : _cfg.rowColor);
|
||
}
|
||
if (_selectedId >= 0 && WLInventory.FindItem(_selectedId) == null) _selectedId = -1;
|
||
|
||
var sel = WLInventory.FindItem(_selectedId);
|
||
_detail.text = sel != null ? WLInventory.Describe(sel)
|
||
: "목록에서 아이템을 고르세요.\n등급: " + WLEquipGrades.Legend(true) + "\n상자 확률(제안값): " + WLEquipGacha.RateText(_cfg.defaultBoxId) +
|
||
"\n던전 클리어 때 FR 드랍표(그룹 " + _cfg.clearDropGroupId + ")로 장비 1개 + 강화석이 들어옵니다.";
|
||
bool isEq = sel != null && sel.itemJson.IsEquipment;
|
||
bool equipped = sel != null && WLInventory.IsEquipped(sel.rawItem._id);
|
||
_equipLabel.text = equipped ? "해제" : "장착";
|
||
_equipBtn.interactable = isEq;
|
||
_lvBtn.interactable = isEq && WLInventory.NextLevelCost(sel) != null;
|
||
_lvAllBtn.interactable = _lvBtn.interactable;
|
||
_composeBtn.interactable = isEq && WLInventory.PlanCompose(sel) != null;
|
||
_downBtn.interactable = isEq && WLEquipTables.FindComposeByResult(sel.itemJson.ItemGrade, sel.itemJson.GradeLv) != null;
|
||
SetButtonColor(_equipBtn, _equipBtn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||
SetButtonColor(_lvBtn, _lvBtn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||
SetButtonColor(_lvAllBtn, _lvAllBtn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||
SetButtonColor(_composeBtn, _composeBtn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||
SetButtonColor(_downBtn, _downBtn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||
|
||
_message.text = WLInventory.LastMessage;
|
||
_gains.text = WLInventory.RecentGains.Count > 0 ? "최근 획득: " + string.Join(" · ", WLInventory.RecentGains.GetRange(0, Mathf.Min(4, WLInventory.RecentGains.Count))) : "";
|
||
}
|
||
|
||
void AddRow()
|
||
{
|
||
int i = _rows.Count;
|
||
var b = NewButton(_listContent, "", _cfg.rowHeightPx, () => OnRowClick(i));
|
||
var lbl = b.GetComponentInChildren<TextMeshProUGUI>();
|
||
lbl.alignment = TextAlignmentOptions.MidlineLeft; lbl.fontSize = _cfg.bodyFontPx;
|
||
lbl.rectTransform.offsetMin = new Vector2(14f, 0f);
|
||
_rows.Add(b); _rowLabels.Add(lbl); _rowBg.Add(b.GetComponent<Image>()); _rowIds.Add(-1);
|
||
_rowBg[i].color = _cfg.rowColor;
|
||
}
|
||
|
||
// ── 조작 ─────────────────────────────────────────────────────────
|
||
void OnRowClick(int i) { if (i < _rowIds.Count) { _selectedId = _rowIds[i]; Refresh(); } }
|
||
void OnSlotClick(int slotIndex)
|
||
{
|
||
var it = WLInventory.EquippedInSlot((eFREquipmentType)(slotIndex + 1));
|
||
if (it != null) { _selectedId = it.rawItem._id; Refresh(); }
|
||
}
|
||
void OnEquipToggle()
|
||
{
|
||
Act(() => WLInventory.IsEquipped(_selectedId) ? WLInventory.Unequip(_selectedId) : WLInventory.Equip(_selectedId));
|
||
}
|
||
void OnCheat()
|
||
{
|
||
WLEquipDrops.Grant(_cfg.cheatGrantGroupId, _cfg.cheatGrantRolls, _cfg.clearStoneItemIds, _cfg.clearStoneCountEach, 0);
|
||
Refresh();
|
||
}
|
||
void Act(System.Func<bool> f)
|
||
{
|
||
try { f(); } catch (System.Exception ex) { Debug.LogError("[WL-821] UI 동작 예외 — " + ex); }
|
||
Refresh();
|
||
}
|
||
|
||
// ── uGUI 헬퍼 ───────────────────────────────────────────────────
|
||
static void Stretch(RectTransform rt)
|
||
{
|
||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; rt.pivot = new Vector2(0.5f, 0.5f);
|
||
}
|
||
static void Pref(RectTransform rt, float h) { var le = rt.gameObject.GetComponent<LayoutElement>() ?? rt.gameObject.AddComponent<LayoutElement>(); le.preferredHeight = h; le.minHeight = h; }
|
||
static void Flex(RectTransform rt, float w) { var le = rt.gameObject.GetComponent<LayoutElement>() ?? rt.gameObject.AddComponent<LayoutElement>(); le.flexibleWidth = w; le.flexibleHeight = w; }
|
||
|
||
Image NewImage(RectTransform parent, string name, Color c)
|
||
{
|
||
var go = new GameObject(name, typeof(RectTransform), typeof(Image));
|
||
go.layer = UILayer; go.transform.SetParent(parent, false);
|
||
var img = go.GetComponent<Image>(); img.color = c; img.raycastTarget = false;
|
||
return img;
|
||
}
|
||
|
||
RectTransform NewRow(RectTransform parent, string name, float h)
|
||
{
|
||
var go = new GameObject(name, typeof(RectTransform), typeof(HorizontalLayoutGroup));
|
||
go.layer = UILayer; go.transform.SetParent(parent, false);
|
||
var hl = go.GetComponent<HorizontalLayoutGroup>();
|
||
hl.spacing = 8f; hl.childControlWidth = true; hl.childControlHeight = true; hl.childForceExpandWidth = false; hl.childForceExpandHeight = true;
|
||
var rt = (RectTransform)go.transform; Pref(rt, h);
|
||
return rt;
|
||
}
|
||
|
||
TextMeshProUGUI NewText(RectTransform parent, string name, float size, TextAlignmentOptions align)
|
||
{
|
||
var go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI));
|
||
go.layer = UILayer; go.transform.SetParent(parent, false);
|
||
var t = go.GetComponent<TextMeshProUGUI>();
|
||
if (_font != null) t.font = _font;
|
||
t.fontSize = size; t.alignment = align; t.raycastTarget = false; t.color = Color.white;
|
||
t.overflowMode = TextOverflowModes.Truncate;
|
||
return t;
|
||
}
|
||
|
||
Button NewButton(RectTransform parent, string label, float width, UnityEngine.Events.UnityAction onClick)
|
||
{
|
||
var go = new GameObject("btn_" + label, typeof(RectTransform), typeof(Image), typeof(Button));
|
||
go.layer = UILayer; go.transform.SetParent(parent, false);
|
||
var img = go.GetComponent<Image>(); img.color = _cfg.buttonColor; img.raycastTarget = true;
|
||
var b = go.GetComponent<Button>(); b.targetGraphic = img; b.onClick.AddListener(onClick);
|
||
var le = go.AddComponent<LayoutElement>();
|
||
if (width > 0f) { le.preferredWidth = width; le.minWidth = width; }
|
||
le.preferredHeight = _cfg.buttonHeightPx;
|
||
var t = NewText((RectTransform)go.transform, "Label", _cfg.bodyFontPx, TextAlignmentOptions.Center);
|
||
t.text = label; Stretch(t.rectTransform);
|
||
return b;
|
||
}
|
||
|
||
static void SetButtonColor(Button b, Color c) { var img = b.GetComponent<Image>(); if (img != null) img.color = c; }
|
||
|
||
// ── 런처 ─────────────────────────────────────────────────────────
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||
static void Boot()
|
||
{
|
||
var cfg = WLEquipSettings.Instance;
|
||
if (cfg == null || cfg.enabled_ == 0 || cfg.launcherEnabled == 0) return;
|
||
WLEquipLauncher.Ensure(cfg);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 인게임(섬·던전)에서 항상 보이는 「장비」 버튼 — 하단 5메뉴는 섬에서 비활성이라(실측 2026-10-08) 별도 진입점이 필요하다.
|
||
/// 자기 Canvas(Overlay) 1장 · 버튼 1개 · 0.5 s 마다 보일 조건(활성 씬 = launcherSceneName · 서버 데이터 있음 · 장비 화면 닫힘)을 다시 본다.
|
||
/// </summary>
|
||
public class WLEquipLauncher : MonoBehaviour
|
||
{
|
||
public static WLEquipLauncher Instance { get; private set; }
|
||
public static int Clicks { get; private set; }
|
||
public bool Visible { get { return _btn != null && _btn.gameObject.activeSelf; } }
|
||
|
||
WLEquipSettings _cfg;
|
||
Button _btn;
|
||
float _timer;
|
||
|
||
public static WLEquipLauncher Ensure(WLEquipSettings cfg)
|
||
{
|
||
if (Instance != null) return Instance;
|
||
var go = new GameObject("WL_EquipLauncher", typeof(RectTransform));
|
||
go.layer = 5;
|
||
DontDestroyOnLoad(go);
|
||
Instance = go.AddComponent<WLEquipLauncher>();
|
||
Instance._cfg = cfg;
|
||
Instance.Build();
|
||
return Instance;
|
||
}
|
||
|
||
void Build()
|
||
{
|
||
var canvas = gameObject.AddComponent<Canvas>();
|
||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||
canvas.sortingOrder = _cfg.uiSortingOrder - 1;
|
||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||
scaler.referenceResolution = new Vector2(1080f, 1920f);
|
||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||
scaler.matchWidthOrHeight = 0.5f;
|
||
gameObject.AddComponent<GraphicRaycaster>();
|
||
|
||
var bgo = new GameObject("btn_equip", typeof(RectTransform), typeof(Image), typeof(Button));
|
||
bgo.layer = 5; bgo.transform.SetParent(transform, false);
|
||
var rt = (RectTransform)bgo.transform;
|
||
rt.anchorMin = rt.anchorMax = new Vector2(1f, 0f); rt.pivot = new Vector2(1f, 0f);
|
||
rt.sizeDelta = _cfg.launcherSizePx;
|
||
rt.anchoredPosition = new Vector2(-_cfg.launcherOffsetPx.x, _cfg.launcherOffsetPx.y);
|
||
var img = bgo.GetComponent<Image>(); img.color = _cfg.buttonColor;
|
||
_btn = bgo.GetComponent<Button>(); _btn.targetGraphic = img;
|
||
_btn.onClick.AddListener(() => { Clicks++; WLEquipUi.Open(); });
|
||
|
||
var tgo = new GameObject("Label", typeof(RectTransform), typeof(TextMeshProUGUI));
|
||
tgo.layer = 5; tgo.transform.SetParent(bgo.transform, false);
|
||
var t = tgo.GetComponent<TextMeshProUGUI>();
|
||
var trt = t.rectTransform; trt.anchorMin = Vector2.zero; trt.anchorMax = Vector2.one; trt.offsetMin = trt.offsetMax = Vector2.zero;
|
||
t.text = _cfg.launcherLabel; t.fontSize = _cfg.bodyFontPx; t.alignment = TextAlignmentOptions.Center; t.raycastTarget = false;
|
||
_label = t;
|
||
_btn.gameObject.SetActive(false);
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
_timer += Time.unscaledDeltaTime;
|
||
if (_timer < 0.5f) return;
|
||
_timer = 0f;
|
||
WLEquipStats.EnforceBaseAttackTick(); // PD 「기본 공격력 5」 — Reset 이 돌 때마다 원본 공식으로 돌아오므로 0.5 s 마다 다시 맞춘다
|
||
bool show = false;
|
||
try
|
||
{
|
||
show = _cfg != null && _cfg.enabled_ != 0 && _cfg.launcherEnabled != 0 && !WLEquipUi.IsOpen &&
|
||
UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == _cfg.launcherSceneName &&
|
||
ServerInfo.Ins != null && ServerInfo.Ins.m_ServerData != null;
|
||
}
|
||
catch { show = false; }
|
||
if (_btn != null && _btn.gameObject.activeSelf != show)
|
||
{
|
||
// 폰트는 처음 보일 때(인게임) 고른다 — 타이틀 시점에 고르면 인게임 한글 폰트(Font SDF)가 아직 없다(2026-10-08 실측 「장□」).
|
||
if (show && !_fontResolved)
|
||
{
|
||
_fontResolved = true;
|
||
var kf = WLEquipUi.ResolveKoreanFont(_cfg);
|
||
if (kf != null && _label != null) _label.font = kf;
|
||
}
|
||
_btn.gameObject.SetActive(show);
|
||
}
|
||
}
|
||
|
||
TextMeshProUGUI _label;
|
||
bool _fontResolved;
|
||
public string FontName { get { return _label != null && _label.font != null ? _label.font.name : "null"; } }
|
||
}
|
||
}
|