940 lines
65 KiB
C#
940 lines
65 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLEquipGui.cs — 장비 화면 GUI v3 · 레퍼런스(캐릭터 중심) 형태 재설계 + Layer Lab 스프라이트 (2026-10-12 · PD 지시)
|
||
//
|
||
// 허브 화면(레퍼런스 1): 상단(자동 장착 · 골드/열쇠(+상자)/강화석) → 캐릭터 무대(3D 미리보기 RenderTexture · 좌 3·우 3 장착 슬롯 ·
|
||
// 공격/생명 알약) → 부위 탭 7 + 「합성」 버튼 → 5열 그리드(등급 색 프레임 · 부위 핀 · Lv · 장착됨 태그) → 뒤로 가기.
|
||
// 상세 팝업(장착/강화/합성/강등) · 뽑기 시트(WLEquipGui.Gacha.cs) · 합성 화면(WLEquipGui.Compose.cs · 레퍼런스 2 · FR ComposePopup 흐름).
|
||
// 코드로 만드는 uGUI(프리팹 0). 치수·색·스프라이트 이름은 전부 `Resources/WL/WLEquipGui.json`(C45). 로직은 WLInventory·WLEquipStats·WLEquipGacha.
|
||
// 스프라이트가 없으면 색 사각형으로 대체해 기능은 그대로 돈다.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System.Collections.Generic;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.UI;
|
||
|
||
namespace WL.Equip
|
||
{
|
||
public partial class WLEquipGui : MonoBehaviour
|
||
{
|
||
public static WLEquipGui Instance { get; private set; }
|
||
public static bool IsOpen { get { return Instance != null && Instance.gameObject.activeSelf; } }
|
||
public static int Opens { get; private set; }
|
||
public static string LastLog { get; private set; } = "";
|
||
|
||
const int UILayer = 5;
|
||
const float RefW = 1080f, RefH = 1920f;
|
||
|
||
WLEquipSettings _cfg;
|
||
WLEquipGuiConfig _g;
|
||
WLGuiLayout L; WLGuiHub H; WLGuiCompose C; WLGuiGacha G;
|
||
TMP_FontAsset _font;
|
||
Canvas _canvas;
|
||
RectTransform _root;
|
||
|
||
// ── 허브 ──
|
||
RectTransform _hub, _gridContent;
|
||
ScrollRect _scroll;
|
||
RawImage _preview;
|
||
WLEquipPreview _previewRig;
|
||
TextMeshProUGUI _coinText, _keyText, _stoneText, _atkText, _hpText, _message;
|
||
readonly Cell[] _slots = new Cell[6];
|
||
readonly TextMeshProUGUI[] _slotNames = new TextMeshProUGUI[6];
|
||
readonly List<Button> _tabBtns = new List<Button>();
|
||
readonly List<Image> _tabIcons = new List<Image>();
|
||
readonly List<Image> _tabLines = new List<Image>();
|
||
int _tab;
|
||
readonly List<Cell> _cells = new List<Cell>();
|
||
Button _autoBtn, _composeOpenBtn, _cheatBtn, _backBtn;
|
||
|
||
// ── 상세 팝업 ──
|
||
GameObject _popup;
|
||
Cell _popupForm;
|
||
TextMeshProUGUI _popupName, _popupRarity, _popupHeaderName, _popupHeaderSub, _popupDps, _popupOptions;
|
||
Image _popupRarityImg;
|
||
Button _equipBtn, _lvBtn, _lvAllBtn, _composeBtn, _downBtn;
|
||
TextMeshProUGUI _equipLabel;
|
||
int _selectedId = -1;
|
||
|
||
/// <summary>그리드 셀·장착 슬롯·결과 카드 공용 조각.</summary>
|
||
class Cell
|
||
{
|
||
public RectTransform root;
|
||
public Image back, icon, frame, glow, pinBg, pin, lvBg, tagBg, badge, badgeIcon, sBadge, check, dim, lockIcon, plusBg;
|
||
public TextMeshProUGUI level, tag, empty, name, plus;
|
||
public Button btn;
|
||
public int id = -1;
|
||
public float size;
|
||
public bool slotStyle;
|
||
}
|
||
|
||
// ── 진입 ─────────────────────────────────────────────────────────
|
||
public static WLEquipGui Open()
|
||
{
|
||
var cfg = WLEquipSettings.Instance;
|
||
if (cfg == null || cfg.enabled_ == 0) return null;
|
||
if (Instance == null)
|
||
{
|
||
var go = new GameObject("WL_EquipGui", typeof(RectTransform));
|
||
go.layer = UILayer;
|
||
DontDestroyOnLoad(go);
|
||
Instance = go.AddComponent<WLEquipGui>();
|
||
Instance._cfg = cfg;
|
||
Instance._g = WLEquipGuiAssets.Config;
|
||
Instance.L = Instance._g.layout ?? new WLGuiLayout();
|
||
Instance.H = Instance._g.hub ?? new WLGuiHub();
|
||
Instance.C = Instance._g.compose ?? new WLGuiCompose();
|
||
Instance.G = Instance._g.gacha ?? new WLGuiGacha();
|
||
try { Instance.Build(); }
|
||
catch (System.Exception ex) { Debug.LogError("[WL-824] GUI 조립 예외 — " + ex); }
|
||
}
|
||
Instance.gameObject.SetActive(true);
|
||
if (Instance._previewRig != null) Instance._previewRig.SetVisible(true);
|
||
Opens++;
|
||
WLInventory.EnsureLoaded();
|
||
Instance.ShowHub();
|
||
return Instance;
|
||
}
|
||
|
||
public static void Close() { if (Instance != null) Instance.gameObject.SetActive(false); }
|
||
|
||
void OnEnable() { WLInventory.Changed += Refresh; if (_previewRig != null) _previewRig.SetVisible(true); }
|
||
void OnDisable()
|
||
{
|
||
WLInventory.Changed -= Refresh;
|
||
_drawCo = null; _revealing = false; _skipReveal = false; // 비활성화되면 코루틴이 멈추므로 상태도 같이 비운다
|
||
if (_previewRig != null) _previewRig.SetVisible(false);
|
||
}
|
||
void OnDestroy() { if (Instance == this) Instance = null; }
|
||
|
||
// ── 조립 ─────────────────────────────────────────────────────────
|
||
void Build()
|
||
{
|
||
_font = WLEquipUi.ResolveKoreanFont(_cfg);
|
||
if (EventSystem.current == null)
|
||
{
|
||
var es = new GameObject("WL_EventSystem(EquipGui)", typeof(EventSystem), typeof(StandaloneInputModule));
|
||
DontDestroyOnLoad(es);
|
||
}
|
||
_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(RefW, RefH);
|
||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||
scaler.matchWidthOrHeight = 0.5f;
|
||
gameObject.AddComponent<GraphicRaycaster>();
|
||
_root = (RectTransform)transform;
|
||
|
||
// 배경 — 단색 + 아래쪽 세로 그라데이션(Layer Lab background_gradient_00)
|
||
var bg = NewImage(_root, "Bg", null, Col("bg", new Color(0.17f, 0.14f, 0.21f, 1f)), Image.Type.Simple);
|
||
Stretch(bg.rectTransform); bg.raycastTarget = true;
|
||
var grad = NewImage(_root, "Grad", Spr("bgGradient"), Spr("bgGradient") != null ? Col("bgBottom", Color.black) : new Color(0f, 0f, 0f, 0f));
|
||
grad.rectTransform.anchorMin = new Vector2(0f, 0f); grad.rectTransform.anchorMax = new Vector2(1f, 0.55f);
|
||
grad.rectTransform.offsetMin = grad.rectTransform.offsetMax = Vector2.zero;
|
||
|
||
BuildHub();
|
||
IP = _g.itemPopup ?? new WLGuiItemPopup();
|
||
if (IpOn) BuildItemPopupV2(); else BuildDetailPopup(); // #881 PD 레퍼런스 팝업(WLEquipGui.RefLayout.cs)
|
||
BuildCompose();
|
||
BuildBoxPopup();
|
||
BuildResultView();
|
||
BuildRatesPopup();
|
||
BuildPartSkillUi(); // #879 장비 파츠 스킬(WLEquipGui.PartSkill.cs)
|
||
LastLog ="조립 완료 · 스프라이트 미발견 " + WLEquipGuiAssets.Missing + (WLEquipGuiAssets.MissingNames.Count > 0 ? " (" + string.Join(",", WLEquipGuiAssets.MissingNames.ToArray()) + ")" : "");
|
||
}
|
||
|
||
/// <summary>가로 띠(위에서 top px ~ bottom px · bottom ≤ 0 이면 바닥까지) — 캐주얼 톤의 상단 띠·아래 남색 패널.</summary>
|
||
Image AddBand(RectTransform parent, string name, float top, float bottom, string colorKey, string spriteKey = null)
|
||
{
|
||
var spr = spriteKey != null ? Spr(spriteKey) : null;
|
||
var img = NewImage(parent, name, spr, Col(colorKey, new Color(0f, 0f, 0f, 0f)), spr != null ? Image.Type.Sliced : Image.Type.Simple);
|
||
var rt = img.rectTransform;
|
||
rt.anchorMin = new Vector2(0f, bottom > 0f ? 1f : 0f); rt.anchorMax = new Vector2(1f, 1f); rt.pivot = new Vector2(0.5f, 1f);
|
||
rt.offsetMax = new Vector2(0f, -top);
|
||
rt.offsetMin = new Vector2(0f, bottom > 0f ? -bottom : 0f);
|
||
return img;
|
||
}
|
||
|
||
void BuildHub()
|
||
{
|
||
_hub = NewRT(_root, "Hub"); Stretch(_hub);
|
||
if (H.topBarFill > 0f) AddBand(_hub, "TopBar", 0f, H.topBarFill, "topBar");
|
||
if (H.lowerPanelTop > 0f) AddBand(_hub, "LowerPanel", H.lowerPanelTop, 0f, "lowerPanel");
|
||
|
||
// 상단 — 자동 장착(레퍼런스의 좌상단 버튼 자리) + 재화 알약 3개(오른쪽부터 강화석 · 열쇠(+상자) · 골드)
|
||
_autoBtn = NewButton(_hub, "자동 장착", Spr("btnPrimary"), Color.white, H.autoBtnW, H.autoBtnH, EquipBest, H.autoFontPx, DarkCol());
|
||
Place((RectTransform)_autoBtn.transform, new Vector2(0f, 1f), new Vector2(H.edge, -H.edge), new Vector2(H.autoBtnW, H.autoBtnH), new Vector2(0f, 1f));
|
||
float px = -H.edge;
|
||
_stoneText = NewPill(_hub, "Stone", "stoneIcon", new Vector2(1f, 1f), new Vector2(px, -H.edge - 10f), false, null); px -= H.pillW + H.pillGap + H.pillIcon * 0.5f;
|
||
_keyText = NewPill(_hub, "Key", "keyIcon", new Vector2(1f, 1f), new Vector2(px, -H.edge - 10f), true, OpenSheet); px -= H.pillW + H.pillGap + H.pillIcon * 0.5f + H.pillAddBtn * 0.5f;
|
||
_coinText = NewPill(_hub, "Coin", "goldIcon", new Vector2(1f, 1f), new Vector2(px, -H.edge - 10f), false, null);
|
||
|
||
// 캐릭터 무대
|
||
var stage = NewRT(_hub, "Stage");
|
||
stage.anchorMin = new Vector2(0f, 1f); stage.anchorMax = new Vector2(1f, 1f); stage.pivot = new Vector2(0.5f, 1f);
|
||
stage.anchoredPosition = new Vector2(0f, -H.stageTop); stage.sizeDelta = new Vector2(0f, H.stageH);
|
||
var glow = NewImage(stage, "Glow", Spr("charGlow"), Spr("charGlow") != null ? Col("charGlowTint", new Color(1f, 1f, 1f, 0.55f)) : new Color(1f, 1f, 1f, 0f), Image.Type.Simple);
|
||
Place(glow.rectTransform, new Vector2(0.5f, 0.5f), new Vector2(0f, H.glowY), new Vector2(H.glowW, H.glowH));
|
||
var plat = NewImage(stage, "Platform", Spr("platform"), Spr("platform") != null ? Col("platformTint", Color.white) : new Color(0f, 0f, 0f, 0.3f), Image.Type.Simple);
|
||
if (H.shadowAtFeet > 0f)
|
||
{
|
||
// 발밑 그림자 — 미리보기 카메라(직교 · 반높이 camOrtho · 높이 camY)에서 모델 원점(발)이 그려지는 y 를 계산해 그 자리에 중심을 둔다.
|
||
// 발 = RenderTexture 아래에서 (camOrtho − camY)/(2·camOrtho) 비율 위 · RawImage 는 무대 가운데 + previewY.
|
||
float feetFromBottom = (H.camOrtho - H.camY) / (2f * H.camOrtho) * H.previewH;
|
||
float feetY = H.previewY - H.previewH * 0.5f + feetFromBottom + H.platformY; // platformY = 미세 보정
|
||
Place(plat.rectTransform, new Vector2(0.5f, 0.5f), new Vector2(0f, feetY), new Vector2(H.platformW, H.platformH), new Vector2(0.5f, 0.5f));
|
||
}
|
||
else Place(plat.rectTransform, new Vector2(0.5f, 0f), new Vector2(0f, H.platformY), new Vector2(H.platformW, H.platformH), new Vector2(0.5f, 0f));
|
||
var prt = NewRT(stage, "Preview");
|
||
_preview = prt.gameObject.AddComponent<RawImage>(); _preview.raycastTarget = false; _preview.color = Color.white;
|
||
Place(prt, new Vector2(0.5f, 0.5f), new Vector2(0f, H.previewY), new Vector2(H.previewW, H.previewH));
|
||
try
|
||
{
|
||
_previewRig = WLEquipPreview.Ensure(H);
|
||
_preview.texture = _previewRig.Texture;
|
||
_previewRig.SetVisible(true); // OnEnable 은 리그가 생기기 전에 지나갔다 — 여기서 켠다(2026-10-12 스모크: 카메라 꺼진 채 빈 무대)
|
||
}
|
||
catch (System.Exception ex) { Debug.LogWarning("[WL-846] 3D 미리보기 리그 생성 실패 — " + ex.Message); _preview.enabled = false; }
|
||
|
||
// 장착 슬롯 — 자리별 부위 = hub.slotTypes(#879 PD 10-16: 왼쪽 무기 · 장갑 · 반지 / 오른쪽 갑옷 · 신발 · 목걸이)
|
||
for (int i = 0; i < 6; i++)
|
||
{
|
||
int slotIndex = i;
|
||
bool left = i < 3; int row = i % 3;
|
||
var c = NewCell(stage, "Slot" + i, H.slotSize, H.slotIconInset, H.slotFontPx, H.slotPin != 0, true, () => OnSlotClick(slotIndex));
|
||
var anchor = left ? new Vector2(0f, 1f) : new Vector2(1f, 1f);
|
||
float x = left ? H.slotX + H.slotSize * 0.5f : -(H.slotX + H.slotSize * 0.5f);
|
||
float y = -(H.slotTop + row * (H.slotSize + H.slotGap + H.slotNameFontPx + 6f) + H.slotSize * 0.5f);
|
||
Place(c.root, anchor, new Vector2(x, y), new Vector2(H.slotSize, H.slotSize), new Vector2(0.5f, 0.5f));
|
||
_slots[i] = c;
|
||
var nm = NewText(stage, "SlotName" + i, H.slotNameFontPx, TextAlignmentOptions.Top, Col("textDim", Color.white));
|
||
Place(nm.rectTransform, anchor, new Vector2(x, y - H.slotSize * 0.5f - 4f), new Vector2(H.slotSize + 40f, H.slotNameFontPx + 8f), new Vector2(0.5f, 1f));
|
||
nm.text = WLEquipTables.SlotName(SlotType(i));
|
||
_slotNames[i] = nm;
|
||
if (H.slotNameFontPx <= 0f) nm.gameObject.SetActive(false); // #881 레퍼런스 = 슬롯 아래 글자 없음(부위 = 핀)
|
||
}
|
||
|
||
// 공격 / 생명 알약(레퍼런스 CP · HP)
|
||
_atkText = NewStatPill(stage, "Atk", "atkIcon", "공격", new Vector2(-(H.statPillW * 0.5f + H.statGap * 0.5f), -H.statY), Col("atk", Color.white));
|
||
_hpText = NewStatPill(stage, "Hp", "hpIcon", "생명", new Vector2(H.statPillW * 0.5f + H.statGap * 0.5f, -H.statY), Col("hp", Color.white));
|
||
|
||
// 부위 탭 7 + 합성 버튼 (캐주얼 톤 = 남색 띠 위에 아이콘 · 선택 탭은 밑줄)
|
||
if (H.tabStripPad > 0f || Col("tabStrip", new Color(0f, 0f, 0f, 0f)).a > 0f)
|
||
AddBand(_hub, "TabStrip", H.tabTop - H.tabStripPad, H.tabTop + H.tabH + H.tabStripPad, "tabStrip");
|
||
if (H.headerMode != 0) BuildGridHeader(); // #881 레퍼런스 = 「정렬 · 나의 장비 · 합성」 머리줄(WLEquipGui.RefLayout.cs)
|
||
else BuildPartTabs();
|
||
|
||
// 그리드
|
||
_scroll = BuildGrid(_hub, "Grid", H.gridTop, H.gridBottom, out _gridContent);
|
||
|
||
// 뒤로 가기 · 메시지 · 테스트 획득
|
||
BuildHubBottom();
|
||
}
|
||
|
||
void BuildPartTabs()
|
||
{
|
||
var tabRow = NewRT(_hub, "Tabs");
|
||
tabRow.anchorMin = new Vector2(0f, 1f); tabRow.anchorMax = new Vector2(1f, 1f); tabRow.pivot = new Vector2(0.5f, 1f);
|
||
tabRow.anchoredPosition = new Vector2(0f, -H.tabTop); tabRow.sizeDelta = new Vector2(0f, H.tabH);
|
||
var hl = tabRow.gameObject.AddComponent<HorizontalLayoutGroup>();
|
||
hl.padding = new RectOffset((int)H.edge, (int)H.edge, 0, 0); hl.spacing = H.tabGap;
|
||
hl.childControlWidth = true; hl.childControlHeight = true; hl.childForceExpandWidth = false; hl.childForceExpandHeight = true; hl.childAlignment = TextAnchor.MiddleLeft;
|
||
var tabs = _g.tabs != null && _g.tabs.Count > 0 ? _g.tabs : new List<WLGuiTab> { new WLGuiTab { id = 0, label = "전체" } };
|
||
for (int i = 0; i < tabs.Count; i++)
|
||
{
|
||
int idx = i;
|
||
var b = NewButton(tabRow, "", Spr("tabBg"), Color.white, H.tabH, H.tabH, () => { _tab = tabs[idx].id; Refresh(); }, H.tabH * 0.3f, TextCol());
|
||
var ic = NewImage((RectTransform)b.transform, "Icon", WLEquipGuiAssets.Get(tabs[i].icon), Color.white, Image.Type.Simple); ic.preserveAspect = true;
|
||
Place(ic.rectTransform, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(H.tabIcon, H.tabIcon));
|
||
if (ic.sprite == null) SetButtonLabel(b, tabs[i].label);
|
||
Image line = null;
|
||
if (H.tabLineH > 0f)
|
||
{
|
||
line = NewImage((RectTransform)b.transform, "Line", Spr("tabLineSpr"), Col("tabLine", new Color(0.2f, 0.55f, 1f, 1f)));
|
||
var lrt = line.rectTransform; lrt.anchorMin = new Vector2(0f, 0f); lrt.anchorMax = new Vector2(1f, 0f); lrt.pivot = new Vector2(0.5f, 0f);
|
||
lrt.anchoredPosition = new Vector2(0f, -H.tabStripPad); lrt.sizeDelta = new Vector2(0f, H.tabLineH);
|
||
}
|
||
_tabBtns.Add(b); _tabIcons.Add(ic); _tabLines.Add(line);
|
||
}
|
||
var spacer = NewRT(tabRow, "Spacer"); Flex(spacer, 1f);
|
||
_composeOpenBtn = NewButton(tabRow, "합성", Spr("btnPrimary"), Color.white, H.composeBtnW, H.composeBtnH, () => OpenCompose(-1), H.composeFontPx, DarkCol());
|
||
}
|
||
|
||
void BuildHubBottom()
|
||
{
|
||
_backBtn = NewBackButton(_hub, Close);
|
||
_cheatBtn = NewButton(_hub, "테스트 획득", Spr("btnNeutral"), Color.white, H.cheatBtnW, H.cheatBtnH, () => { WLEquipDrops.Grant(_cfg.cheatGrantGroupId, _cfg.cheatGrantRolls, _cfg.clearStoneItemIds, _cfg.clearStoneCountEach, 0); Refresh(); }, H.messageFontPx, TextCol());
|
||
Place((RectTransform)_cheatBtn.transform, new Vector2(1f, 0f), new Vector2(-H.edge, H.backY + (H.backBtn - H.cheatBtnH) * 0.5f), new Vector2(H.cheatBtnW, H.cheatBtnH), new Vector2(1f, 0f));
|
||
_cheatBtn.gameObject.SetActive(_cfg.cheatGrantEnabled != 0);
|
||
_message = NewText(_hub, "Message", H.messageFontPx, TextAlignmentOptions.MidlineLeft, Col("textDim", Color.white));
|
||
_message.enableWordWrapping = true; _message.overflowMode = TextOverflowModes.Ellipsis;
|
||
var mrt = _message.rectTransform; mrt.anchorMin = new Vector2(0f, 0f); mrt.anchorMax = new Vector2(1f, 0f); mrt.pivot = new Vector2(0.5f, 0f);
|
||
mrt.offsetMin = new Vector2(BackRight() + 14f, H.backY); mrt.offsetMax = new Vector2(-(H.edge + (_cfg.cheatGrantEnabled != 0 ? H.cheatBtnW + 14f : 0f)), H.backY + H.backBtn);
|
||
}
|
||
|
||
float BackRight() { return (H.backFlushLeft > 0f ? 0f : H.edge) + (H.backBtnW > 0f ? H.backBtnW : H.backBtn); }
|
||
|
||
/// <summary>뒤로 가기(레퍼런스 = 왼쪽 가장자리에 붙은 어두운 탭 + 화살표).</summary>
|
||
Button NewBackButton(RectTransform parent, UnityEngine.Events.UnityAction onClick)
|
||
{
|
||
float w = H.backBtnW > 0f ? H.backBtnW : H.backBtn;
|
||
var b = NewIconButton(parent, "Back", "backIcon", H.backBtn, onClick, "backBtnBg");
|
||
Place((RectTransform)b.transform, new Vector2(0f, 0f), new Vector2(H.backFlushLeft > 0f ? 0f : H.edge, H.backY), new Vector2(w, H.backBtn), new Vector2(0f, 0f));
|
||
return b;
|
||
}
|
||
|
||
/// <summary>스크롤 그리드(허브·합성 공용) — 패널 배경 + 뷰포트 + GridLayout 콘텐츠.</summary>
|
||
ScrollRect BuildGrid(RectTransform parent, string name, float top, float bottom, out RectTransform content)
|
||
{
|
||
var listGo = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(ScrollRect));
|
||
listGo.layer = UILayer; listGo.transform.SetParent(parent, false);
|
||
var lrt = (RectTransform)listGo.transform;
|
||
lrt.anchorMin = new Vector2(0f, 0f); lrt.anchorMax = new Vector2(1f, 1f); lrt.pivot = new Vector2(0.5f, 1f);
|
||
lrt.offsetMin = new Vector2(H.edge, RefH - bottom); lrt.offsetMax = new Vector2(-H.edge, -top);
|
||
var listImg = listGo.GetComponent<Image>();
|
||
listImg.sprite = Spr("panelBg"); listImg.type = SlicedIf(listImg.sprite); listImg.color = Col("panel", new Color(0f, 0f, 0f, 0.35f)); listImg.raycastTarget = true;
|
||
var 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, 8f, 8f, 8f, 8f);
|
||
viewport.GetComponent<Image>().color = new Color(0f, 0f, 0f, 0.01f);
|
||
var cgo = new GameObject("Content", typeof(RectTransform), typeof(GridLayoutGroup), typeof(ContentSizeFitter));
|
||
cgo.layer = UILayer; cgo.transform.SetParent(viewport.transform, false);
|
||
content = (RectTransform)cgo.transform;
|
||
content.anchorMin = new Vector2(0f, 1f); content.anchorMax = new Vector2(1f, 1f); content.pivot = new Vector2(0.5f, 1f);
|
||
content.offsetMin = Vector2.zero; content.offsetMax = Vector2.zero;
|
||
var grid = cgo.GetComponent<GridLayoutGroup>();
|
||
grid.cellSize = new Vector2(H.gridCell, H.gridCell);
|
||
grid.spacing = new Vector2(H.gridGap, H.gridGap);
|
||
grid.padding = new RectOffset((int)H.gridPad, (int)H.gridPad, (int)H.gridPad, (int)H.gridPad);
|
||
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount; grid.constraintCount = Mathf.Max(1, H.gridCols);
|
||
grid.childAlignment = TextAnchor.UpperCenter;
|
||
cgo.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||
scroll.content = content; scroll.viewport = vrt; scroll.horizontal = false; scroll.vertical = true;
|
||
scroll.movementType = ScrollRect.MovementType.Clamped; scroll.scrollSensitivity = 40f;
|
||
return scroll;
|
||
}
|
||
|
||
/// <summary>재화 알약(Layer Lab status_frame) — 왼쪽에 아이콘이 튀어나오고 오른쪽 끝에 「+」 버튼(열쇠 = 상자 시트).</summary>
|
||
TextMeshProUGUI NewPill(RectTransform parent, string name, string iconKey, Vector2 anchor, Vector2 pos, bool withAdd, UnityEngine.Events.UnityAction onAdd)
|
||
{
|
||
var box = NewImage(parent, name, Spr("pillBg"), Col("pill", new Color(0.1f, 0.08f, 0.13f, 0.92f)));
|
||
Place(box.rectTransform, anchor, pos, new Vector2(H.pillW, H.pillH), new Vector2(1f, 1f));
|
||
var icon = NewImage(box.rectTransform, "Icon", Spr(iconKey), Color.white, Image.Type.Simple); icon.preserveAspect = true;
|
||
Place(icon.rectTransform, new Vector2(0f, 0.5f), new Vector2(-H.pillIcon * 0.25f, 0f), new Vector2(H.pillIcon, H.pillIcon), new Vector2(0f, 0.5f));
|
||
var t = NewText(box.rectTransform, "Value", H.pillFontPx, TextAlignmentOptions.MidlineRight, TextCol());
|
||
t.fontStyle = FontStyles.Bold;
|
||
var trt = t.rectTransform; trt.anchorMin = Vector2.zero; trt.anchorMax = Vector2.one; trt.offsetMin = new Vector2(H.pillIcon * 0.8f, 0f); trt.offsetMax = new Vector2(withAdd ? -(H.pillAddBtn * 0.6f + 6f) : -14f, 0f);
|
||
if (withAdd)
|
||
{
|
||
var add = NewButton(box.rectTransform, "", Spr("pillAdd"), Color.white, H.pillAddBtn, H.pillAddBtn, onAdd, H.pillFontPx + 8f, TextCol()); MakeSimple(add);
|
||
Place((RectTransform)add.transform, new Vector2(1f, 0.5f), new Vector2(H.pillAddBtn * 0.3f, 0f), new Vector2(H.pillAddBtn, H.pillAddBtn), new Vector2(1f, 0.5f));
|
||
var plus = Spr("pillAddIcon");
|
||
if (plus != null) { var pi = NewImage((RectTransform)add.transform, "Icon", plus, Color.white, Image.Type.Simple); pi.preserveAspect = true; Stretch(pi.rectTransform, 10f, 10f, 10f, 10f); }
|
||
else SetButtonLabel(add, "+");
|
||
}
|
||
return t;
|
||
}
|
||
|
||
/// <summary>공격/생명 알약 — 아이콘 + 작은 제목 + 큰 수치. 누르면 전체 스탯 한 줄을 메시지에 띄운다.</summary>
|
||
TextMeshProUGUI NewStatPill(RectTransform parent, string name, string iconKey, string caption, Vector2 pos, Color captionCol)
|
||
{
|
||
var box = NewImage(parent, name, Spr("statPill"), Col("statPill", new Color(0.1f, 0.08f, 0.13f, 0.92f))); box.raycastTarget = true;
|
||
Place(box.rectTransform, new Vector2(0.5f, 1f), pos, new Vector2(H.statPillW, H.statPillH), new Vector2(0.5f, 0.5f));
|
||
var b = box.gameObject.AddComponent<Button>(); b.targetGraphic = box; b.onClick.AddListener(() => { _message.text = WLEquipStats.StatusLine(); });
|
||
var icon = NewImage(box.rectTransform, "Icon", Spr(iconKey), Color.white, Image.Type.Simple); icon.preserveAspect = true;
|
||
Place(icon.rectTransform, new Vector2(0f, 0.5f), new Vector2(-H.statIcon * 0.2f, 0f), new Vector2(H.statIcon, H.statIcon), new Vector2(0f, 0.5f));
|
||
var cap = NewText(box.rectTransform, "Caption", H.statFontPx * 0.55f, TextAlignmentOptions.BottomLeft, captionCol); cap.text = caption; cap.fontStyle = FontStyles.Bold;
|
||
var crt = cap.rectTransform; crt.anchorMin = new Vector2(0f, 0f); crt.anchorMax = new Vector2(0.42f, 1f); crt.offsetMin = new Vector2(H.statIcon * 0.9f, 10f); crt.offsetMax = new Vector2(0f, -8f);
|
||
var t = NewText(box.rectTransform, "Value", H.statFontPx, TextAlignmentOptions.MidlineRight, TextCol()); t.fontStyle = FontStyles.Bold;
|
||
var trt = t.rectTransform; trt.anchorMin = new Vector2(0.35f, 0f); trt.anchorMax = new Vector2(1f, 1f); trt.offsetMin = Vector2.zero; trt.offsetMax = new Vector2(-18f, 0f);
|
||
return t;
|
||
}
|
||
|
||
/// <summary>팝업·시트 상자 — Layer Lab popup_01_frame 은 속이 비쳐 보이므로 어두운 바탕을 먼저 깔고 프레임을 얹는다.</summary>
|
||
Image NewPopupBox(RectTransform parent, string name, Vector2 size)
|
||
{
|
||
var fill = NewImage(parent, name, Spr("innerBox"), Col("popupFill", new Color(0.14f, 0.11f, 0.17f, 0.98f))); fill.raycastTarget = true;
|
||
var frt = fill.rectTransform; frt.anchorMin = frt.anchorMax = new Vector2(0.5f, 0.5f); frt.pivot = new Vector2(0.5f, 0.5f); frt.sizeDelta = size - new Vector2(28f, 28f);
|
||
var frame = NewImage(parent, name + "Frame", Spr("popupBg"), Color.white);
|
||
var brt = frame.rectTransform; brt.anchorMin = brt.anchorMax = new Vector2(0.5f, 0.5f); brt.pivot = new Vector2(0.5f, 0.5f); brt.sizeDelta = size;
|
||
return frame;
|
||
}
|
||
|
||
Button NewIconButton(RectTransform parent, string name, string iconKey, float size, UnityEngine.Events.UnityAction onClick, string bgKey = "squareBtn")
|
||
{
|
||
var b = NewButton(parent, name, Spr(bgKey) ?? Spr("squareBtn"), Color.white, size, size, onClick, size * 0.3f, TextCol());
|
||
var ic = NewImage((RectTransform)b.transform, "Icon", Spr(iconKey), Color.white, Image.Type.Simple); ic.preserveAspect = true;
|
||
Place(ic.rectTransform, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(size * 0.6f, size * 0.6f));
|
||
SetButtonLabel(b, ic.sprite == null ? "<" : "");
|
||
return b;
|
||
}
|
||
|
||
// ── 상세 팝업(Layer Lab popup_01_frame — 위 150 px 띠가 제목 자리) ──
|
||
void BuildDetailPopup()
|
||
{
|
||
_popup = new GameObject("DetailPopup", typeof(RectTransform)); _popup.layer = UILayer; _popup.transform.SetParent(_root, false);
|
||
var prt = (RectTransform)_popup.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(() => _popup.SetActive(false));
|
||
|
||
var box = NewPopupBox(prt, "Box", new Vector2(L.popupW, L.popupH));
|
||
var brt = box.rectTransform;
|
||
|
||
// 제목 띠
|
||
var titleArea = NewRT(brt, "TitleArea");
|
||
titleArea.anchorMin = new Vector2(0f, 1f); titleArea.anchorMax = new Vector2(1f, 1f); titleArea.pivot = new Vector2(0.5f, 1f);
|
||
titleArea.anchoredPosition = Vector2.zero; titleArea.sizeDelta = new Vector2(0f, L.popupTitleH);
|
||
_popupName = NewText(titleArea, "Name", L.popupNameFontPx, TextAlignmentOptions.Midline, TextCol()); _popupName.fontStyle = FontStyles.Bold;
|
||
var nrt = _popupName.rectTransform; nrt.anchorMin = new Vector2(0f, 0.35f); nrt.anchorMax = new Vector2(1f, 1f); nrt.offsetMin = new Vector2(110f, 0f); nrt.offsetMax = new Vector2(-110f, -14f);
|
||
_popupRarityImg = NewImage(titleArea, "RarityBg", Spr("gradeLabel"), Color.white);
|
||
Place(_popupRarityImg.rectTransform, new Vector2(0.5f, 0f), new Vector2(0f, 6f), new Vector2(300f, 48f), new Vector2(0.5f, 0f));
|
||
_popupRarity = NewText(_popupRarityImg.rectTransform, "Rarity", L.popupRarityFontPx, TextAlignmentOptions.Midline, Color.white); _popupRarity.fontStyle = FontStyles.Bold; Stretch(_popupRarity.rectTransform);
|
||
var closeBtn = NewIconButton(titleArea, "Close", "closeIcon", L.closeBtnW, () => _popup.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(36f, 36f); body.offsetMax = new Vector2(-36f, -L.popupTitleH - 10f);
|
||
var vl = body.gameObject.AddComponent<VerticalLayoutGroup>();
|
||
vl.padding = new RectOffset(0, 0, 0, 0); vl.spacing = 14f;
|
||
vl.childControlWidth = true; vl.childControlHeight = true; vl.childForceExpandWidth = true; vl.childForceExpandHeight = false;
|
||
|
||
var header = NewImage(body, "Header", Spr("innerBox"), Col("innerBox", new Color(0f, 0f, 0f, 0.35f))); Pref(header.rectTransform, 0f, L.headerH);
|
||
_popupForm = NewCell(header.rectTransform, "Form", L.headerForm, L.cellIconInset, L.cellFontPx, true, false, null);
|
||
Place(_popupForm.root, new Vector2(0f, 0.5f), new Vector2(14f, 0f), new Vector2(L.headerForm, L.headerForm), new Vector2(0f, 0.5f));
|
||
_popupHeaderName = NewText(header.rectTransform, "HName", L.headerNameFontPx, TextAlignmentOptions.TopLeft, TextCol()); _popupHeaderName.fontStyle = FontStyles.Bold;
|
||
var hn = _popupHeaderName.rectTransform; hn.anchorMin = new Vector2(0f, 1f); hn.anchorMax = new Vector2(1f, 1f); hn.pivot = new Vector2(0f, 1f); hn.offsetMin = new Vector2(L.headerForm + 30f, -64f); hn.offsetMax = new Vector2(-12f, -14f);
|
||
_popupHeaderSub = NewText(header.rectTransform, "HSub", L.headerDpsFontPx, TextAlignmentOptions.TopLeft, Col("textDim", Color.white));
|
||
var hs = _popupHeaderSub.rectTransform; hs.anchorMin = new Vector2(0f, 1f); hs.anchorMax = new Vector2(1f, 1f); hs.pivot = new Vector2(0f, 1f); hs.offsetMin = new Vector2(L.headerForm + 30f, -112f); hs.offsetMax = new Vector2(-12f, -66f);
|
||
var dpsBox = NewImage(header.rectTransform, "DpsBox", Spr("innerBox"), Col("dpsBox", new Color(0f, 0f, 0f, 0.5f)));
|
||
var drt = dpsBox.rectTransform; drt.anchorMin = new Vector2(0f, 0f); drt.anchorMax = new Vector2(1f, 0f); drt.pivot = new Vector2(0f, 0f); drt.offsetMin = new Vector2(L.headerForm + 30f, 14f); drt.offsetMax = new Vector2(-14f, 14f + L.headerDpsH);
|
||
_popupDps = NewText(drt, "Dps", L.headerDpsFontPx, TextAlignmentOptions.MidlineLeft, TextCol()); _popupDps.fontStyle = FontStyles.Bold; Stretch(_popupDps.rectTransform, 16f, 2f, 16f, 2f);
|
||
|
||
var opt = NewImage(body, "Options", Spr("innerBox"), Col("innerBox", new Color(0f, 0f, 0f, 0.35f)));
|
||
var ole = opt.gameObject.AddComponent<LayoutElement>(); ole.flexibleHeight = 1f; ole.minHeight = 200f;
|
||
_popupOptions = NewText(opt.rectTransform, "Text", L.optionFontPx, TextAlignmentOptions.TopLeft, TextCol()); Stretch(_popupOptions.rectTransform, 20f, 16f, 20f, 16f);
|
||
_popupOptions.overflowMode = TextOverflowModes.Truncate; _popupOptions.enableWordWrapping = true;
|
||
|
||
var b1 = NewRow(body, "Buttons1", L.popupBtnH, 10f);
|
||
_equipBtn = NewButton(b1, "장착", Spr("btnPrimary"), Color.white, 0f, L.popupBtnH, OnEquipToggle, L.popupBtnFontPx, DarkCol()); Flex((RectTransform)_equipBtn.transform, 1f);
|
||
_equipLabel = _equipBtn.GetComponentInChildren<TextMeshProUGUI>();
|
||
_lvBtn = NewButton(b1, "강화 +1", Spr("btnSecondary"), Color.white, 0f, L.popupBtnH, () => Act(() => WLInventory.ItemLevelUp(_selectedId, false)), L.popupBtnFontPx, TextCol()); Flex((RectTransform)_lvBtn.transform, 1f);
|
||
_lvAllBtn = NewButton(b1, "전부 강화", Spr("btnSecondary"), Color.white, 0f, L.popupBtnH, () => Act(() => WLInventory.ItemLevelUp(_selectedId, true)), L.popupBtnFontPx, TextCol()); Flex((RectTransform)_lvAllBtn.transform, 1f);
|
||
var b2 = NewRow(body, "Buttons2", L.popupBtnH, 10f);
|
||
_composeBtn = NewButton(b2, "합성", Spr("btnBlue"), Color.white, 0f, L.popupBtnH, () => OpenCompose(_selectedId), L.popupBtnFontPx, TextCol()); Flex((RectTransform)_composeBtn.transform, 1f);
|
||
_downBtn = NewButton(b2, "강등", Spr("btnDanger"), Color.white, 0f, L.popupBtnH, () => Act(() => WLInventory.Downgrade(_selectedId)), L.popupBtnFontPx, TextCol()); Flex((RectTransform)_downBtn.transform, 1f);
|
||
var closeBtn2 = NewButton(b2, "닫기", Spr("btnNeutral"), Color.white, 0f, L.popupBtnH, () => _popup.SetActive(false), L.popupBtnFontPx, TextCol()); Flex((RectTransform)closeBtn2.transform, 1f);
|
||
_popup.SetActive(false);
|
||
}
|
||
|
||
// ── 셀(그리드 · 장착 슬롯 · 결과 카드 공용) ─────────────────────
|
||
/// <param name="withPin">좌상단 부위 핀(그리드용)</param>
|
||
/// <param name="slotStyle">장착 슬롯(프레임 = slotFrame · 빈 칸에 부위 아이콘·이름)</param>
|
||
Cell NewCell(RectTransform parent, string name, float size, float iconInset, float fontPx, bool withPin, bool slotStyle, UnityEngine.Events.UnityAction onClick)
|
||
{
|
||
var c = new Cell(); c.size = size; c.slotStyle = slotStyle;
|
||
c.root = NewRT(parent, name);
|
||
var le = c.root.gameObject.AddComponent<LayoutElement>(); le.preferredWidth = size; le.minWidth = size; le.preferredHeight = size; le.minHeight = size; le.flexibleHeight = 0f;
|
||
// 선택 글로우(맨 뒤 · 셀보다 크게 → 테두리 밖으로 번지는 빛)
|
||
c.glow = NewImage(c.root, "Glow", Spr("selectGlow"), Col("selectGlow", new Color(1f, 0.9f, 0.45f, 1f)));
|
||
Stretch(c.glow.rectTransform, -16f, -16f, -16f, -16f); c.glow.gameObject.SetActive(false);
|
||
// 등급 색 판(Layer Lab 흰 프레임은 속이 찬 모양이라 등급 색으로 물들여 바닥에 깔고, 그 위에 어두운 속을 조금 작게 얹어 테두리만 남긴다) — 버튼 본체
|
||
c.frame = NewImage(c.root, "Frame", Spr(slotStyle ? "slotFrame" : "cellFrame"), Col("frameEmpty", Color.gray));
|
||
Stretch(c.frame.rectTransform); c.frame.raycastTarget = true;
|
||
if (onClick != null) { c.btn = c.frame.gameObject.AddComponent<Button>(); c.btn.targetGraphic = c.frame; c.btn.onClick.AddListener(onClick); }
|
||
// 어두운 속(테두리 두께 = frameRim)
|
||
float rim = slotStyle ? H.slotRim : H.cellRim;
|
||
c.back = NewImage(c.root, "Back", Spr(slotStyle ? "slotInner" : "cellBack"), Col(slotStyle ? "slotInner" : "cellBack", new Color(0.12f, 0.1f, 0.15f, 1f)));
|
||
Stretch(c.back.rectTransform, rim, rim, rim, rim);
|
||
// 아이콘
|
||
c.icon = NewImage(c.root, "Icon", null, Color.white, Image.Type.Simple); c.icon.preserveAspect = true;
|
||
Stretch(c.icon.rectTransform, iconInset, iconInset, iconInset, iconInset);
|
||
// 어둡게(합성 화면에서 재료가 아닌 것) + 자물쇠
|
||
c.dim = NewImage(c.root, "Dim", null, Col("dimCell", new Color(0f, 0f, 0f, 0.62f)), Image.Type.Simple); Stretch(c.dim.rectTransform, 4f, 4f, 4f, 4f);
|
||
c.lockIcon = NewImage(c.dim.rectTransform, "Lock", Spr("lockIcon"), new Color(1f, 1f, 1f, 0.85f), Image.Type.Simple); c.lockIcon.preserveAspect = true;
|
||
Place(c.lockIcon.rectTransform, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(size * 0.36f, size * 0.36f));
|
||
c.dim.gameObject.SetActive(false);
|
||
// 좌상단 부위 핀
|
||
if (withPin)
|
||
{
|
||
c.pinBg = NewImage(c.root, "PinBg", Spr("pinBg"), Col("pinBg", new Color(0.2f, 0.18f, 0.24f, 0.95f)));
|
||
Place(c.pinBg.rectTransform, new Vector2(0f, 1f), new Vector2(4f, -4f), new Vector2(H.cellPin, H.cellPin), new Vector2(0f, 1f));
|
||
c.pin = NewImage(c.pinBg.rectTransform, "Pin", null, Color.white, Image.Type.Simple); c.pin.preserveAspect = true; Stretch(c.pin.rectTransform, 7f, 7f, 7f, 7f);
|
||
c.pinBg.gameObject.SetActive(false);
|
||
}
|
||
// 레벨(슬롯 = 좌상단 · 그리드 = 우하단)
|
||
float lvH = slotStyle ? H.slotLvH : H.cellLvH;
|
||
c.lvBg = NewImage(c.root, "LvBg", Spr("lvBg"), Col("lvBg", new Color(0f, 0f, 0f, 0.65f)));
|
||
if (slotStyle) Place(c.lvBg.rectTransform, new Vector2(0f, 1f), new Vector2(6f, -6f), new Vector2(size * 0.5f, lvH), new Vector2(0f, 1f));
|
||
else Place(c.lvBg.rectTransform, new Vector2(1f, 0f), new Vector2(-6f, 6f), new Vector2(size * 0.5f, lvH), new Vector2(1f, 0f));
|
||
c.level = NewText(c.lvBg.rectTransform, "Level", fontPx - 2f, slotStyle ? TextAlignmentOptions.Midline : TextAlignmentOptions.MidlineRight, Color.white); c.level.fontStyle = FontStyles.Bold; Stretch(c.level.rectTransform); ApplyOutline(c.level);
|
||
c.lvBg.gameObject.SetActive(false);
|
||
// 장착됨 태그(가운데 띠)
|
||
c.tagBg = NewImage(c.root, "TagBg", Spr("tag"), Col("tagBg", new Color(0.08f, 0.08f, 0.1f, 0.9f)));
|
||
var tgr = c.tagBg.rectTransform; tgr.anchorMin = new Vector2(0f, 0.5f); tgr.anchorMax = new Vector2(1f, 0.5f); tgr.pivot = new Vector2(0.5f, 0.5f);
|
||
tgr.anchoredPosition = new Vector2(0f, -size * 0.08f); tgr.sizeDelta = new Vector2(-8f, H.cellTagH);
|
||
c.tag = NewText(tgr, "Tag", fontPx, TextAlignmentOptions.Midline, Color.white); c.tag.fontStyle = FontStyles.Bold; Stretch(c.tag.rectTransform); ApplyOutline(c.tag);
|
||
c.tagBg.gameObject.SetActive(false);
|
||
// 우상단 초록 배지(강화 가능 ↑)
|
||
float bs = slotStyle ? H.slotBadge : H.cellPin;
|
||
c.badge = NewImage(c.root, "Badge", Spr("badgeBg") ?? Spr("pinBg"), Col("upBadge", new Color(0.35f, 0.85f, 0.3f, 1f)), Image.Type.Simple); c.badge.preserveAspect = true;
|
||
Place(c.badge.rectTransform, new Vector2(1f, 1f), new Vector2(bs * 0.25f, bs * 0.25f), new Vector2(bs, bs), new Vector2(1f, 1f));
|
||
c.badgeIcon = NewImage(c.badge.rectTransform, "Icon", Spr("badgeIcon"), Color.white, Image.Type.Simple); c.badgeIcon.preserveAspect = true; Stretch(c.badgeIcon.rectTransform, 8f, 8f, 8f, 8f);
|
||
c.badgeIcon.gameObject.SetActive(c.badgeIcon.sprite != null);
|
||
c.badge.gameObject.SetActive(false);
|
||
// S 등급 표식(우상단 · 배지 없을 때)
|
||
c.sBadge = NewImage(c.root, "S", Spr("sBadge"), new Color(1f, 0.85f, 0.3f, 1f), Image.Type.Simple); c.sBadge.preserveAspect = true;
|
||
Place(c.sBadge.rectTransform, new Vector2(1f, 1f), new Vector2(-4f, -4f), new Vector2(bs * 0.8f, bs * 0.8f), new Vector2(1f, 1f));
|
||
c.sBadge.gameObject.SetActive(false);
|
||
// 체크(합성 재료 선택)
|
||
c.check = NewImage(c.root, "Check", Spr("checkBg") ?? Spr("pinBg"), Col("check", new Color(0.4f, 0.9f, 0.35f, 1f)));
|
||
Place(c.check.rectTransform, new Vector2(1f, 1f), new Vector2(bs * 0.25f, bs * 0.25f), new Vector2(bs, bs), new Vector2(1f, 1f));
|
||
var ck = NewImage(c.check.rectTransform, "Icon", Spr("checkIcon"), Color.white, Image.Type.Simple); ck.preserveAspect = true; Stretch(ck.rectTransform, 8f, 8f, 8f, 8f);
|
||
c.check.gameObject.SetActive(false);
|
||
// 빈 슬롯 글자
|
||
if (slotStyle)
|
||
{
|
||
c.empty = NewText(c.root, "Empty", fontPx, TextAlignmentOptions.Bottom, Col("textDim", Color.white));
|
||
Stretch(c.empty.rectTransform, 6f, 6f, 6f, 10f); c.empty.gameObject.SetActive(false);
|
||
}
|
||
if (H.cellRefLayout != 0) ApplyRefCellLayout(c, size, slotStyle);
|
||
return c;
|
||
}
|
||
|
||
/// <summary>#881 PD 레퍼런스 칸 — Lv = 오른쪽 위 · S = 왼쪽 아래 · +단계 = 아래 가운데 마름모(부위 핀 = 왼쪽 위 그대로).</summary>
|
||
void ApplyRefCellLayout(Cell c, float size, bool slotStyle)
|
||
{
|
||
float lvH = slotStyle ? H.slotLvH : H.cellLvH;
|
||
Place(c.lvBg.rectTransform, new Vector2(1f, 1f), new Vector2(-6f, -6f), new Vector2(size * 0.5f, lvH), new Vector2(1f, 1f));
|
||
c.lvBg.color = new Color(0f, 0f, 0f, 0f); // 레퍼런스 = 바탕 없이 외곽선 글자
|
||
c.level.alignment = TextAlignmentOptions.MidlineRight;
|
||
float bs = slotStyle ? H.slotBadge : H.cellPin;
|
||
Place(c.sBadge.rectTransform, new Vector2(0f, 0f), new Vector2(4f, 4f), new Vector2(bs * 0.8f, bs * 0.8f), new Vector2(0f, 0f));
|
||
c.plusBg = NewImage(c.root, "PlusBg", null, Col("plusBg", new Color(0.16f, 0.12f, 0.3f, 1f)), Image.Type.Simple);
|
||
Place(c.plusBg.rectTransform, new Vector2(0.5f, 0f), new Vector2(0f, 0f), new Vector2(H.plusSize * 0.72f, H.plusSize * 0.72f), new Vector2(0.5f, 0.5f));
|
||
c.plusBg.rectTransform.localRotation = Quaternion.Euler(0f, 0f, 45f);
|
||
c.plus = NewText(c.root, "Plus", H.plusFontPx, TextAlignmentOptions.Midline, Color.white); c.plus.fontStyle = FontStyles.Bold; ApplyOutline(c.plus);
|
||
Place(c.plus.rectTransform, new Vector2(0.5f, 0f), new Vector2(0f, 0f), new Vector2(H.plusSize, H.plusSize), new Vector2(0.5f, 0.5f));
|
||
c.plusBg.gameObject.SetActive(false); c.plus.gameObject.SetActive(false);
|
||
}
|
||
|
||
void SetPlus(Cell c, int plus)
|
||
{
|
||
if (c.plusBg == null) return;
|
||
bool on = plus > 0;
|
||
SetOn(c.plusBg, on); SetOn(c.plus, on);
|
||
if (on) c.plus.text = plus.ToString();
|
||
}
|
||
|
||
static void SetOn(Component c, bool on) { if (c != null && c.gameObject.activeSelf != on) c.gameObject.SetActive(on); }
|
||
|
||
/// <summary>
|
||
/// 칸 바탕 — tier > 0 = 등급 · 0 = 빈 칸 · -1 = 소모품.
|
||
/// tintTierBg 1 = 흰 프레임 1장에 등급 색을 곱한다(판타지 키트) · 0 = 등급마다 색 칸 그림을 그대로 쓴다(캐주얼 키트 ItemFrame01_Single_*).
|
||
/// </summary>
|
||
void PaintFrame(Cell c, int tier)
|
||
{
|
||
if (_g.tintTierBg != 0)
|
||
{
|
||
c.frame.sprite = Spr(c.slotStyle ? "slotFrame" : "cellFrame"); c.frame.type = SlicedIf(c.frame.sprite);
|
||
c.frame.color = tier > 0 ? WLEquipIcons.TierColor(tier) : Col("frameEmpty", Color.gray);
|
||
return;
|
||
}
|
||
Sprite s = tier > 0 ? WLEquipIcons.TierBg(tier) : Spr(tier < 0 ? "cellConsumable" : "cellEmpty");
|
||
if (s == null) s = Spr("cellEmpty");
|
||
c.frame.sprite = s; c.frame.type = SlicedIf(s);
|
||
c.frame.color = s != null ? (tier == 0 ? Col("cellEmptyTint", Color.white) : Color.white) : (tier > 0 ? WLEquipIcons.TierColor(tier) : Col("frameEmpty", Color.gray));
|
||
}
|
||
|
||
Material _outlineMat;
|
||
/// <summary>글자 외곽선 — 글자마다 머티리얼 사본을 만들지 않도록 공유 머티리얼 1장(모바일 드로우콜·메모리).</summary>
|
||
void ApplyOutline(TextMeshProUGUI t)
|
||
{
|
||
if (t == null || _g.textOutline <= 0f || _font == null) return;
|
||
if (_outlineMat == null)
|
||
{
|
||
_outlineMat = new Material(_font.material) { name = "WL_EquipGui_Outline" };
|
||
_outlineMat.EnableKeyword(ShaderUtilities.Keyword_Outline);
|
||
_outlineMat.SetFloat(ShaderUtilities.ID_OutlineWidth, _g.textOutline);
|
||
_outlineMat.SetColor(ShaderUtilities.ID_OutlineColor, Col("textOutline", new Color(0.05f, 0.08f, 0.2f, 1f)));
|
||
_outlineMat.SetFloat(ShaderUtilities.ID_FaceDilate, _g.textOutline * 0.5f);
|
||
}
|
||
t.fontSharedMaterial = _outlineMat;
|
||
}
|
||
|
||
/// <summary>아이템 1건을 셀에 그린다. tagEquipped = 장착 중이면 「장착됨」 띠.</summary>
|
||
void BindCell(Cell c, Item it, bool tagEquipped)
|
||
{
|
||
c.id = it != null ? it.rawItem._id : -1;
|
||
bool has = it != null;
|
||
SetOn(c.lvBg, false); SetOn(c.tagBg, false); SetOn(c.badge, false); SetOn(c.sBadge, false); SetOn(c.check, false); SetOn(c.dim, false); SetOn(c.pinBg, false);
|
||
SetOn(c.glow, false); SetPlus(c, 0);
|
||
if (c.empty != null) SetOn(c.empty, false);
|
||
PaintFrame(c, 0);
|
||
c.icon.color = Color.white;
|
||
SetOn(c.icon, has);
|
||
if (!has) return;
|
||
var j = it.itemJson;
|
||
int tier = 1, plus = 0; WLGradeTier t = null;
|
||
if (j.IsEquipment && WLEquipGrades.TryMap(j, out t, out plus)) tier = t.tier;
|
||
PaintFrame(c, j.IsEquipment ? tier : -1);
|
||
Color tint; var icon = WLEquipIcons.For(j, out tint);
|
||
c.icon.sprite = icon; c.icon.color = icon != null ? tint : new Color(1f, 1f, 1f, 0.15f);
|
||
if (j.IsEquipment)
|
||
{
|
||
SetOn(c.lvBg, true); c.level.text = "Lv." + it.rawItem.level + (plus > 0 && c.plusBg == null ? " +" + plus : "");
|
||
SetPlus(c, plus);
|
||
if (c.pinBg != null) { var ps = WLEquipIcons.PartIcon(j.EquipmentType); c.pin.sprite = ps; SetOn(c.pinBg, ps != null); }
|
||
if (t != null && t.isS != 0) SetOn(c.sBadge, true);
|
||
if (tagEquipped && WLInventory.IsEquipped(c.id)) { SetOn(c.tagBg, true); c.tag.text = "장착됨"; }
|
||
}
|
||
else { SetOn(c.lvBg, true); c.level.text = "×" + it.rawItem.count; }
|
||
}
|
||
|
||
/// <summary>표 행(ItemJson)만으로 셀을 그린다 — 합성 결과 미리보기.</summary>
|
||
void BindCellJson(Cell c, ItemJson j, int level)
|
||
{
|
||
c.id = -1;
|
||
SetOn(c.lvBg, false); SetOn(c.tagBg, false); SetOn(c.badge, false); SetOn(c.sBadge, false); SetOn(c.check, false); SetOn(c.dim, false); SetOn(c.pinBg, false); SetOn(c.glow, false);
|
||
SetPlus(c, 0);
|
||
if (c.empty != null) SetOn(c.empty, false);
|
||
PaintFrame(c, 0);
|
||
SetOn(c.icon, j != null);
|
||
if (j == null) return;
|
||
int tier = 1, plus = 0; WLGradeTier t = null;
|
||
if (WLEquipGrades.TryMap(j, out t, out plus)) tier = t.tier;
|
||
PaintFrame(c, tier);
|
||
Color tint; var icon = WLEquipIcons.For(j, out tint);
|
||
c.icon.sprite = icon; c.icon.color = icon != null ? tint : new Color(1f, 1f, 1f, 0.15f);
|
||
SetOn(c.lvBg, true); c.level.text = "Lv." + level + (plus > 0 && c.plusBg == null ? " +" + plus : "");
|
||
SetPlus(c, plus);
|
||
if (t != null && t.isS != 0) SetOn(c.sBadge, true);
|
||
}
|
||
|
||
/// <summary>장착 슬롯 — 비어 있으면 부위 아이콘을 흐리게 + 이름.</summary>
|
||
void BindSlot(int i)
|
||
{
|
||
var slot = SlotType(i);
|
||
var c = _slots[i];
|
||
var it = WLInventory.EquippedInSlot(slot);
|
||
BindCell(c, it, false);
|
||
if (it == null)
|
||
{
|
||
SetOn(c.icon, true); c.icon.sprite = WLEquipIcons.PartIcon(slot); c.icon.color = new Color(1f, 1f, 1f, c.icon.sprite != null ? 0.28f : 0f);
|
||
return; // 이름은 칸 아래 라벨(_slotNames)이 맡는다
|
||
}
|
||
SetOn(c.badge, CanLevelUpNow(it));
|
||
if (c.badge.gameObject.activeSelf) SetOn(c.sBadge, false);
|
||
SetOn(c.glow, it.rawItem._id == _selectedId && _popup.activeSelf);
|
||
}
|
||
|
||
/// <summary>지금 가진 골드·강화석으로 다음 강화가 가능한가(슬롯의 초록 ↑ 배지).</summary>
|
||
static bool CanLevelUpNow(Item it)
|
||
{
|
||
var cost = WLInventory.NextLevelCost(it);
|
||
if (cost == null) return false;
|
||
if (WLInventory.GetGold() < cost.MoneyCountRequired) return false;
|
||
if (cost.ItemKeyRequired > 0 && cost.ItemCountRequired > 0 && WLInventory.GetItemCountByIndex(cost.ItemKeyRequired) < cost.ItemCountRequired) return false;
|
||
return true;
|
||
}
|
||
|
||
// ── 갱신 ─────────────────────────────────────────────────────────
|
||
void ShowHub()
|
||
{
|
||
if (_cView != null) _cView.gameObject.SetActive(false);
|
||
_hub.gameObject.SetActive(true);
|
||
_popup.SetActive(false);
|
||
ClosePartSkillPopups();
|
||
Refresh();
|
||
}
|
||
|
||
public void Refresh()
|
||
{
|
||
if (!gameObject.activeSelf) return;
|
||
if (!WLInventory.EnsureLoaded()) { if (_message != null) _message.text = "장비 표 로드 실패 — " + WLEquipTables.LastLog; return; }
|
||
try { RefreshInner(); }
|
||
catch (System.Exception ex) { Debug.LogError("[WL-824] Refresh 예외 — " + ex); }
|
||
}
|
||
|
||
void RefreshInner()
|
||
{
|
||
_coinText.text = Short(WLInventory.GetGold());
|
||
_keyText.text = WLInventory.GetKeys().ToString();
|
||
long stones = 0;
|
||
if (_cfg.clearStoneItemIds != null) for (int i = 0; i < _cfg.clearStoneItemIds.Length; i++) stones += WLInventory.GetItemCountByIndex(_cfg.clearStoneItemIds[i]);
|
||
_stoneText.text = stones.ToString();
|
||
|
||
var info = WLEquipStats.LivePcInfo();
|
||
if (info != null) { try { WLEquipStats.EnforceBaseAttack(info); } catch { } }
|
||
_atkText.text = info != null ? Short(info.Get_Stat(eStat.ATK)) : "-";
|
||
_hpText.text = info != null ? Short(info.Get_Stat(eStat.MaxHP)) : "-";
|
||
|
||
for (int i = 0; i < 6; i++) BindSlot(i);
|
||
RefreshPartSkillUi(); // #879 슬롯 능력치 · 스킬 뱃지 · 장착 스킬 줄 · 시너지 알림
|
||
|
||
var tabs = _g.tabs;
|
||
for (int i = 0; i < _tabBtns.Count; i++)
|
||
{
|
||
bool on = tabs != null && i < tabs.Count && tabs[i].id == _tab;
|
||
var img = _tabBtns[i].GetComponent<Image>();
|
||
if (img != null) img.color = Col(on ? "tabOnBg" : "tabOffBg", on ? Color.white : new Color(0.62f, 0.6f, 0.66f, 1f));
|
||
_tabIcons[i].color = Col(on ? "tabOn" : "tabOff", Color.white);
|
||
_tabBtns[i].transform.localScale = on ? Vector3.one * 1.08f : Vector3.one;
|
||
if (i < _tabLines.Count && _tabLines[i] != null) SetOn(_tabLines[i], on);
|
||
}
|
||
|
||
var items = GridItems(WLInventory.SortedItems()); // #881 장착분 빼기 · 정렬(머리줄)
|
||
RefreshGridHeader();
|
||
int n = 0;
|
||
for (int i = 0; i < items.Count; i++)
|
||
{
|
||
var it = items[i];
|
||
if (_tab != 0 && (!it.itemJson.IsEquipment || (int)it.itemJson.EquipmentType != _tab)) continue;
|
||
if (n >= _cells.Count)
|
||
{
|
||
int idx = n;
|
||
_cells.Add(NewCell(_gridContent, "Cell" + n, H.gridCell, H.cellIconInset, H.cellFontPx, true, false, () => OnCellClick(idx)));
|
||
}
|
||
var c = _cells[n];
|
||
c.root.gameObject.SetActive(true);
|
||
BindCell(c, it, true);
|
||
SetOn(c.glow, c.id == _selectedId && _popup.activeSelf);
|
||
n++;
|
||
if (n >= _cfg.maxListRows) break;
|
||
}
|
||
for (int i = n; i < _cells.Count; i++) _cells[i].root.gameObject.SetActive(false);
|
||
if (_selectedId >= 0 && WLInventory.FindItem(_selectedId) == null) { _selectedId = -1; _popup.SetActive(false); }
|
||
|
||
_message.text = WLInventory.LastMessage;
|
||
if (_previewRig != null) { try { _previewRig.Refresh(); } catch (System.Exception ex) { Debug.LogWarning("[WL-846] 미리보기 갱신 실패 — " + ex.Message); } }
|
||
if (_popup.activeSelf) RefreshPopup();
|
||
if (_boxPopup != null && _boxPopup.activeSelf) RefreshBox();
|
||
if (_cView != null && _cView.gameObject.activeSelf) RefreshCompose();
|
||
}
|
||
|
||
void RefreshPopup()
|
||
{
|
||
var sel = WLInventory.FindItem(_selectedId);
|
||
if (sel == null) { _popup.SetActive(false); return; }
|
||
if (IpOn) { RefreshItemPopupV2(sel); return; } // #881 레퍼런스 팝업
|
||
var j = sel.itemJson;
|
||
BindCell(_popupForm, sel, false);
|
||
_popupName.text = j.Item_name;
|
||
int tier = 1, plus = 0; WLGradeTier t = null;
|
||
if (j.IsEquipment && WLEquipGrades.TryMap(j, out t, out plus)) tier = t.tier;
|
||
_popupRarityImg.color = j.IsEquipment ? TierCol(tier) : Col("frameEmpty", Color.gray);
|
||
_popupRarity.text = j.IsEquipment ? WLEquipGrades.Label(j, false) : "소모품";
|
||
bool isEq = j.IsEquipment;
|
||
if (isEq)
|
||
{
|
||
_popupHeaderName.text = j.Item_name + " <size=80%>" + WLEquipTables.SlotName(j.EquipmentType) + "</size>";
|
||
_popupHeaderSub.text = "Lv " + sel.rawItem.level + " / " + j.ItemMaxLv + (WLInventory.IsEquipped(sel.rawItem._id) ? " · 장착 중" : "");
|
||
eStat o; double bv, bonus; string line;
|
||
_popupDps.text = WLEquipStats.ItemValues(sel, WLEquipStats.CurrentMainStat(), out o, out bv, out bonus, out line)
|
||
? WL.PartSkill.WLPartStats.ItemStatLine(sel, false) + " <size=75%>(기본 " + bv.ToString("F1") + " + 스탯 " + bonus.ToString("F1") + ")</size>"
|
||
: WL.PartSkill.WLPartStats.ItemStatLine(sel, false); // #879 신발 이동 속도 · 목걸이 랜덤 스탯 = 배율
|
||
}
|
||
else { _popupHeaderName.text = j.Item_name; _popupHeaderSub.text = "보유 ×" + sel.rawItem.count; _popupDps.text = ""; }
|
||
_popupOptions.text = PartSkillPopupText(sel) // #879 스킬 · 시너지(맨 위)
|
||
+ WLInventory.Describe(sel)
|
||
+ (isEq && j.EquipmentType == eFREquipmentType.Weapon ? "\n<color=#F0C070>공격 형태: " + WL.Combat.Weapon.WLWeaponAttackTable.Describe(j.GroupNo) + "</color>" : "");
|
||
bool equipped = isEq && WLInventory.IsEquipped(sel.rawItem._id);
|
||
_equipLabel.text = equipped ? "해제" : "장착";
|
||
SetEnabled(_equipBtn, isEq, "btnPrimary");
|
||
bool canLv = isEq && WLInventory.NextLevelCost(sel) != null;
|
||
SetEnabled(_lvBtn, canLv, "btnSecondary"); SetEnabled(_lvAllBtn, canLv, "btnSecondary");
|
||
ItemComposeJson cj; ItemJson rj;
|
||
SetEnabled(_composeBtn, isEq && WLInventory.ComposeInfo(sel, out cj, out rj), "btnBlue");
|
||
SetEnabled(_downBtn, isEq && WLEquipTables.FindComposeByResult(j.ItemGrade, j.GradeLv) != null, "btnDanger");
|
||
}
|
||
|
||
// ── 조작 ─────────────────────────────────────────────────────────
|
||
void OnCellClick(int idx)
|
||
{
|
||
if (idx < 0 || idx >= _cells.Count) return;
|
||
_selectedId = _cells[idx].id;
|
||
if (_selectedId < 0) return;
|
||
_popup.SetActive(true);
|
||
Refresh();
|
||
}
|
||
|
||
void OnSlotClick(int slotIndex)
|
||
{
|
||
var it = WLInventory.EquippedInSlot(SlotType(slotIndex));
|
||
if (it == null) { _tab = (int)SlotType(slotIndex); Refresh(); return; }
|
||
_selectedId = it.rawItem._id;
|
||
_popup.SetActive(true);
|
||
Refresh();
|
||
}
|
||
|
||
void OnEquipToggle() { Act(() => WLInventory.IsEquipped(_selectedId) ? WLInventory.Unequip(_selectedId) : WLInventory.Equip(_selectedId)); }
|
||
|
||
/// <summary>부위별로 (등급, 단계, 레벨)이 가장 높은 장비를 장착.</summary>
|
||
void EquipBest()
|
||
{
|
||
var best = new Dictionary<int, Item>();
|
||
var score = new Dictionary<int, long>();
|
||
foreach (var it in WLInventory.SortedItems())
|
||
{
|
||
if (!it.itemJson.IsEquipment) continue;
|
||
int tier = 1, plus = 0; WLGradeTier t;
|
||
if (WLEquipGrades.TryMap(it.itemJson, out t, out plus)) tier = t.tier;
|
||
long s = tier * 1000000L + plus * 10000L + it.rawItem.level;
|
||
int part = (int)it.itemJson.EquipmentType;
|
||
long cur;
|
||
if (!score.TryGetValue(part, out cur) || s > cur) { score[part] = s; best[part] = it; }
|
||
}
|
||
int changed = 0;
|
||
foreach (var kv in best) if (!WLInventory.IsEquipped(kv.Value.rawItem._id)) { if (WLInventory.Equip(kv.Value.rawItem._id)) changed++; }
|
||
WLInventory.Commit(changed > 0 ? "자동 장착 — " + changed + "부위 교체" : "자동 장착 — 이미 최선");
|
||
Refresh();
|
||
}
|
||
|
||
void Act(System.Func<bool> f)
|
||
{
|
||
try { f(); } catch (System.Exception ex) { Debug.LogError("[WL-824] GUI 동작 예외 — " + ex); }
|
||
Refresh();
|
||
}
|
||
|
||
// 프로브(CLI 스모크)용 — 셀 탭·버튼과 같은 경로
|
||
public void SelectForProbe(int id) { _selectedId = id; _popup.SetActive(true); Refresh(); }
|
||
public void ClosePopupsForProbe()
|
||
{
|
||
_popup.SetActive(false); if (_boxPopup != null) _boxPopup.SetActive(false);
|
||
if (_resultView != null) _resultView.SetActive(false);
|
||
if (_ratesPopup != null) _ratesPopup.SetActive(false);
|
||
_drawCo = null; _revealing = false; _skipReveal = false;
|
||
ShowHub();
|
||
}
|
||
public void SetTabForProbe(int tab) { _tab = tab; Refresh(); }
|
||
public void EquipBestForProbe() { EquipBest(); }
|
||
public bool PopupVisibleForProbe { get { return _popup != null && _popup.activeSelf; } }
|
||
public bool HubVisibleForProbe { get { return _hub != null && _hub.gameObject.activeSelf; } }
|
||
public int VisibleCellsForProbe { get { int n = 0; for (int i = 0; i < _cells.Count; i++) if (_cells[i].root.gameObject.activeSelf) n++; return n; } }
|
||
public string PreviewStateForProbe
|
||
{
|
||
get
|
||
{
|
||
return _previewRig == null ? "rig null" : ("model=" + _previewRig.HasModel + " loading=" + _previewRig.Loading + " class=" + _previewRig.ShownClassId + " prefab=" + _previewRig.ShownPrefab + " loads=" + WLEquipPreview.Loads + " applies=" + WLEquipPreview.ClassApplies + " · " + WLEquipPreview.LastLog);
|
||
}
|
||
}
|
||
|
||
// ── uGUI 헬퍼 ───────────────────────────────────────────────────
|
||
Sprite Spr(string key) { return WLEquipGuiAssets.GetByKey(key); }
|
||
Color Col(string key, Color fallback) { return _g.Col(key, fallback); }
|
||
Color TextCol() { return _g.Col("text", new Color(0.96f, 0.94f, 0.9f, 1f)); }
|
||
Color DarkCol() { return _g.Col("textDark", new Color(0.28f, 0.17f, 0.05f, 1f)); }
|
||
/// <summary>등급 색(라벨·확률 알약 등 흰 그림에 곱하는 색) — 칸 바탕은 PaintFrame 이 따로 정한다.</summary>
|
||
Color TierCol(int tier) { return WLEquipIcons.TierColor(tier); }
|
||
static Image.Type SlicedIf(Sprite s) { return s != null && s.border.sqrMagnitude > 0f ? Image.Type.Sliced : Image.Type.Simple; }
|
||
|
||
/// <summary>4.86K · 19.5K · 1.2M 식 짧은 수치(레퍼런스 CP/HP 표기).</summary>
|
||
static string Short(double v)
|
||
{
|
||
double a = System.Math.Abs(v);
|
||
if (a >= 1e9) return (v / 1e9).ToString("0.##") + "B";
|
||
if (a >= 1e6) return (v / 1e6).ToString("0.##") + "M";
|
||
if (a >= 1e4) return (v / 1e3).ToString("0.#") + "K";
|
||
if (a >= 1e3) return (v / 1e3).ToString("0.##") + "K";
|
||
return v.ToString("0");
|
||
}
|
||
|
||
static RectTransform NewRT(RectTransform parent, string name)
|
||
{
|
||
var go = new GameObject(name, typeof(RectTransform)); go.layer = UILayer; go.transform.SetParent(parent, false);
|
||
return (RectTransform)go.transform;
|
||
}
|
||
|
||
static void Stretch(RectTransform rt, float l = 0f, float t = 0f, float r = 0f, float b = 0f)
|
||
{
|
||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.pivot = new Vector2(0.5f, 0.5f);
|
||
rt.offsetMin = new Vector2(l, b); rt.offsetMax = new Vector2(-r, -t);
|
||
}
|
||
static void Pref(RectTransform rt, float w, float h)
|
||
{
|
||
var le = rt.gameObject.GetComponent<LayoutElement>() ?? rt.gameObject.AddComponent<LayoutElement>();
|
||
if (w > 0f) { le.preferredWidth = w; le.minWidth = w; }
|
||
// flexibleHeight 0 명시 — 가로 줄이 flexible 1 을 보고해 세로 레이아웃이 남는 공간을 나눠 주던 결함(2026-10-09 계측)
|
||
if (h > 0f) { le.preferredHeight = h; le.minHeight = h; le.flexibleHeight = 0f; }
|
||
}
|
||
static void MakeSimple(Button b)
|
||
{
|
||
var img = b.GetComponent<Image>();
|
||
if (img == null) return;
|
||
img.type = Image.Type.Simple; img.preserveAspect = true;
|
||
}
|
||
static void Flex(RectTransform rt, float w)
|
||
{
|
||
var le = rt.gameObject.GetComponent<LayoutElement>() ?? rt.gameObject.AddComponent<LayoutElement>();
|
||
le.flexibleWidth = w;
|
||
}
|
||
/// <summary>앵커 한 점 배치(레이아웃 그룹 밖) — pivot 생략 시 앵커와 같다.</summary>
|
||
static void Place(RectTransform rt, Vector2 anchor, Vector2 pos, Vector2 size, Vector2? pivot = null)
|
||
{
|
||
rt.anchorMin = rt.anchorMax = anchor; rt.pivot = pivot ?? anchor; rt.anchoredPosition = pos; rt.sizeDelta = size;
|
||
}
|
||
|
||
Image NewImage(RectTransform parent, string name, Sprite spr, Color color, Image.Type type = Image.Type.Sliced)
|
||
{
|
||
var rt = NewRT(parent, name);
|
||
var img = rt.gameObject.AddComponent<Image>();
|
||
img.sprite = spr; img.color = color; img.raycastTarget = false;
|
||
img.type = type == Image.Type.Sliced ? SlicedIf(spr) : type;
|
||
return img;
|
||
}
|
||
|
||
RectTransform NewRow(RectTransform parent, string name, float h, float spacing)
|
||
{
|
||
var rt = NewRT(parent, name);
|
||
var hl = rt.gameObject.AddComponent<HorizontalLayoutGroup>();
|
||
hl.spacing = spacing; hl.childControlWidth = true; hl.childControlHeight = true; hl.childForceExpandWidth = false; hl.childForceExpandHeight = true;
|
||
Pref(rt, 0f, h);
|
||
return rt;
|
||
}
|
||
|
||
TextMeshProUGUI NewText(RectTransform parent, string name, float size, TextAlignmentOptions align, Color color)
|
||
{
|
||
var rt = NewRT(parent, name);
|
||
var t = rt.gameObject.AddComponent<TextMeshProUGUI>();
|
||
if (_font != null) t.font = _font;
|
||
t.fontSize = size; t.alignment = align; t.raycastTarget = false; t.color = color;
|
||
t.overflowMode = TextOverflowModes.Truncate; t.richText = true;
|
||
return t;
|
||
}
|
||
|
||
Button NewButton(RectTransform parent, string label, Sprite spr, Color color, float w, float h, UnityEngine.Events.UnityAction onClick, float fontPx, Color textColor)
|
||
{
|
||
var rt = NewRT(parent, "btn_" + label);
|
||
var img = rt.gameObject.AddComponent<Image>();
|
||
img.sprite = spr; img.type = SlicedIf(spr); img.color = spr != null ? color : new Color(0.32f, 0.36f, 0.5f, 1f); img.raycastTarget = true;
|
||
var b = rt.gameObject.AddComponent<Button>(); b.targetGraphic = img; b.onClick.AddListener(onClick);
|
||
var le = rt.gameObject.AddComponent<LayoutElement>();
|
||
if (w > 0f) { le.preferredWidth = w; le.minWidth = w; }
|
||
le.preferredHeight = h; le.minHeight = h; le.flexibleHeight = 0f;
|
||
var t = NewText(rt, "Label", fontPx, TextAlignmentOptions.Midline, textColor);
|
||
t.text = label; t.fontStyle = FontStyles.Bold; Stretch(t.rectTransform, 6f, 2f, 6f, 6f);
|
||
ApplyOutline(t);
|
||
return b;
|
||
}
|
||
|
||
static void SetButtonLabel(Button b, string label) { var t = b.GetComponentInChildren<TextMeshProUGUI>(); if (t != null) t.text = label; }
|
||
|
||
void SetEnabled(Button b, bool on, string spriteKey)
|
||
{
|
||
if (b == null) return;
|
||
b.interactable = on;
|
||
var img = b.GetComponent<Image>();
|
||
if (img == null) return;
|
||
var spr = on ? Spr(spriteKey) : (Spr("btnDisabledSpr") ?? Spr(spriteKey));
|
||
img.sprite = spr; img.type = SlicedIf(spr);
|
||
img.color = on ? (spr != null ? Color.white : new Color(0.32f, 0.36f, 0.5f, 1f)) : (Spr("btnDisabledSpr") != null ? Color.white : Col("btnDisabled", new Color(0.45f, 0.45f, 0.48f, 1f)));
|
||
var lbl = b.GetComponentInChildren<TextMeshProUGUI>();
|
||
if (lbl != null) { var c = lbl.color; c.a = on ? 1f : 0.55f; lbl.color = c; }
|
||
}
|
||
}
|
||
}
|