Project_WL/Assets/WL/PartSkill/UI/WLEquipGui.PartSkill.cs

509 lines
38 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.PartSkill.cs — 장비 화면 #879 장비 파츠 스킬 확장 (기획 v2 18 · 19장 · PD 10-16 슬롯 배치)
//
// - 슬롯: 자리별 부위(hub.slotTypes) · 칸 아래 「부위 + 기본 능력치」 2줄 · 칸 모서리 스킬 뱃지(공격 빨강 · 방어 파랑 · 패시브 초록 · 무기 시너지 보라 · 시너지 금테)
// - 무대 아래 「장착 스킬」 줄: 액티브 4(원) + 패시브 2(띠) + 시너지 도감 버튼 · 그 아래 시너지 알림 한 줄
// - 스킬 상세 팝업: 설명 · 등급별 개방 표(일반 Lv1 … 전설 Lv5 · 불멸 × 1.2 · 신화 × 1.5 · 지금 등급 강조) · 시너지 조건 · 룬 칸(이후)
// - 시너지 도감: 시너지 · 결합 목록 · 찾은 것 ✓ · 못 찾은 것은 짝을 ??? 로 가림
// - 아이템 상세 팝업 맨 위에 스킬 · 시너지 문단
// 치수 = WLEquipGui.json partSkill · 색 = colors(skillAttack · skillDefense · skillPassive · skillWeapon · synergyOn …) · C45.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using WL.PartSkill;
namespace WL.Equip
{
public partial class WLEquipGui
{
WLGuiPartSkill PS;
sealed class PsBadge { public RectTransform root; public Image ring, bg, icon; public TextMeshProUGUI lv; public WLPartSlot slot; }
sealed class PsChip { public RectTransform root; public Image ring, bg, icon; public TextMeshProUGUI lv, name; public WLPartSlot slot; public bool passive; }
readonly PsBadge[] _psBadges = new PsBadge[6];
readonly List<PsChip> _psChips = new List<PsChip>();
RectTransform _psStrip;
TextMeshProUGUI _psSyn;
Button _psBookBtn;
// 스킬 상세
GameObject _psDetail;
Image _psdIcon, _psdRing;
TextMeshProUGUI _psdTitle, _psdSub, _psdDesc, _psdSyn, _psdRune;
readonly List<Image> _psdRowBg = new List<Image>();
readonly List<TextMeshProUGUI> _psdRowTier = new List<TextMeshProUGUI>(), _psdRowText = new List<TextMeshProUGUI>();
readonly List<Image> _psdRowTierBg = new List<Image>();
// 도감
GameObject _psBook;
RectTransform _psBookContent;
TextMeshProUGUI _psBookHead;
readonly List<TextMeshProUGUI> _psBookRows = new List<TextMeshProUGUI>();
readonly List<Image> _psBookRowBg = new List<Image>();
readonly List<string> _psBookKeys = new List<string>();
public static int PartSkillDetailOpens, PartSkillBookOpens;
bool PsOn { get { return PS != null && PS.enabled != 0 && WLPartSkillData.Enabled; } }
/// <summary>슬롯 자리 i(왼쪽 위 0 … 오른쪽 아래 5)의 부위.</summary>
eFREquipmentType SlotType(int i)
{
var t = H != null ? H.slotTypes : null;
return t != null && i < t.Length && t[i] > 0 ? (eFREquipmentType)t[i] : (eFREquipmentType)(i + 1);
}
Color AttrCol(string attr, bool synergy)
{
if (synergy) return Col("synergyOn", new Color(1f, 0.8f, 0.25f, 1f));
switch (attr)
{
case "attack": return Col("skillAttack", new Color(0.93f, 0.33f, 0.33f, 1f));
case "defense": return Col("skillDefense", new Color(0.27f, 0.56f, 0.96f, 1f));
case "passive": return Col("skillPassive", new Color(0.32f, 0.76f, 0.45f, 1f));
case "weapon": return Col("skillWeapon", new Color(0.62f, 0.43f, 0.9f, 1f));
}
return Col("frameEmpty", Color.gray);
}
// ── 조립 ─────────────────────────────────────────────────────────
void BuildPartSkillUi()
{
PS = _g.partSkill ?? new WLGuiPartSkill();
if (!PsOn) return;
WLPartSkillState.EnsureHooked();
WLPartSkillState.SynergyFound -= OnPsSynergyFound;
WLPartSkillState.SynergyFound += OnPsSynergyFound;
var circle = WLPartSkillHud.CircleSprite();
// 슬롯 아래 라벨 2줄 + 스킬 뱃지
for (int i = 0; i < 6; i++)
{
var nm = _slotNames[i];
if (nm != null && nm.gameObject.activeSelf)
{
var rt = nm.rectTransform; rt.sizeDelta = new Vector2(H.slotSize + 70f, PS.slotLabelH);
nm.fontSize = PS.slotLabelFontPx; nm.enableWordWrapping = false; nm.overflowMode = TextOverflowModes.Overflow; nm.lineSpacing = -8f;
}
var c = _slots[i]; if (c == null) continue;
var b = new PsBadge();
b.root = NewRT(c.root, "SkillBadge");
Place(b.root, new Vector2(1f, 0f), new Vector2(-PS.badgeInset, PS.badgeInset), new Vector2(PS.badge, PS.badge), new Vector2(1f, 0f));
b.ring = NewImage(b.root, "Ring", circle, Color.white, Image.Type.Simple); Stretch(b.ring.rectTransform); b.ring.raycastTarget = true;
b.bg = NewImage(b.root, "Bg", circle, Col("slotInner", new Color(0.12f, 0.1f, 0.15f, 1f)), Image.Type.Simple); Stretch(b.bg.rectTransform, 5f, 5f, 5f, 5f);
b.icon = NewImage(b.root, "Icon", null, Color.white, Image.Type.Simple); b.icon.preserveAspect = true; Stretch(b.icon.rectTransform, 8f, 8f, 8f, 8f);
b.lv = NewText(b.root, "Lv", PS.badgeLvFontPx, TextAlignmentOptions.Bottom, Color.white); b.lv.fontStyle = FontStyles.Bold; ApplyOutline(b.lv);
var lrt = b.lv.rectTransform; lrt.anchorMin = new Vector2(0f, 0f); lrt.anchorMax = new Vector2(1f, 0f); lrt.pivot = new Vector2(0.5f, 1f); lrt.pivot = new Vector2(0.5f, 0.5f); lrt.anchoredPosition = new Vector2(0f, 4f); lrt.sizeDelta = new Vector2(20f, PS.badgeLvFontPx + 6f);
var btn = b.ring.gameObject.AddComponent<Button>(); btn.targetGraphic = b.ring;
var bb = b; btn.onClick.AddListener(() => { if (bb.slot != null) OpenSkillDetail(bb.slot); });
b.root.gameObject.SetActive(false);
_psBadges[i] = b;
}
// 장착 스킬 줄
_psStrip = NewRT(_hub, "PartSkillStrip");
_psStrip.anchorMin = new Vector2(0f, 1f); _psStrip.anchorMax = new Vector2(1f, 1f); _psStrip.pivot = new Vector2(0.5f, 1f);
_psStrip.anchoredPosition = new Vector2(0f, -PS.stripTop); _psStrip.sizeDelta = new Vector2(0f, PS.stripH);
var hl = _psStrip.gameObject.AddComponent<HorizontalLayoutGroup>();
hl.padding = new RectOffset((int)PS.stripPad, (int)PS.stripPad, 0, 0); hl.spacing = PS.chipGap;
hl.childControlWidth = true; hl.childControlHeight = true; hl.childForceExpandWidth = false; hl.childForceExpandHeight = false;
hl.childAlignment = PS.stripCenter != 0 ? TextAnchor.MiddleCenter : TextAnchor.MiddleLeft;
for (int i = 0; i < 4; i++) _psChips.Add(NewChip(false, circle));
for (int i = 0; i < 2; i++) _psChips.Add(NewChip(true, circle));
if (PS.stripCenter == 0) { var spacer = NewRT(_psStrip, "Spacer"); Flex(spacer, 1f); }
if (PS.bookRound != 0)
{ // #881 레퍼런스 = 둥근 메뉴 단추 줄 — 도감도 같은 크기의 원
_psBookBtn = NewButton(_psStrip, "도감", circle, Col("bookBtn", new Color(0.32f, 0.76f, 0.45f, 1f)), PS.chip, PS.chip, OpenSynergyBook, PS.bookBtnFontPx, TextCol());
_psBookBtn.GetComponent<Image>().type = Image.Type.Simple;
}
else _psBookBtn = NewButton(_psStrip, "도감", Spr("btnSecondary"), Color.white, PS.bookBtnW, PS.bookBtnH, OpenSynergyBook, PS.bookBtnFontPx, TextCol());
_psSyn = NewText(_hub, "PartSkillSynergy", PS.synFontPx, TextAlignmentOptions.MidlineLeft, TextCol());
var srt = _psSyn.rectTransform; srt.anchorMin = new Vector2(0f, 1f); srt.anchorMax = new Vector2(1f, 1f); srt.pivot = new Vector2(0.5f, 1f);
srt.anchoredPosition = new Vector2(0f, -PS.synTop); srt.sizeDelta = new Vector2(-PS.stripPad * 2f, Mathf.Max(1f, PS.synH));
_psSyn.enableWordWrapping = false; _psSyn.overflowMode = TextOverflowModes.Ellipsis;
if (PS.synH <= 0f) _psSyn.gameObject.SetActive(false); // #881 글자 줄이기 — 시너지는 칩 금테 · 상세 팝업에서
BuildSkillDetail(circle);
BuildSynergyBook();
}
PsChip NewChip(bool passive, Sprite circle)
{
var c = new PsChip { passive = passive };
bool pill = passive && PS.passiveRound == 0; // #881 레퍼런스 = 패시브도 원(이름 없이 아이콘 + Lv)
float w = pill ? PS.passiveW : PS.chip, h = pill ? PS.passiveH : PS.chip;
c.root = NewRT(_psStrip, passive ? "Passive" : "Active");
var le = c.root.gameObject.AddComponent<LayoutElement>(); le.preferredWidth = le.minWidth = w; le.preferredHeight = le.minHeight = h;
c.ring = NewImage(c.root, "Ring", pill ? Spr("innerBox") : circle, Color.white, pill ? Image.Type.Sliced : Image.Type.Simple); Stretch(c.ring.rectTransform); c.ring.raycastTarget = true;
c.bg = NewImage(c.root, "Bg", pill ? Spr("innerBox") : circle, Col("slotInner", new Color(0.12f, 0.1f, 0.15f, 1f)), pill ? Image.Type.Sliced : Image.Type.Simple); Stretch(c.bg.rectTransform, 5f, 5f, 5f, 5f);
c.icon = NewImage(c.root, "Icon", null, Color.white, Image.Type.Simple); c.icon.preserveAspect = true;
if (pill) Place(c.icon.rectTransform, new Vector2(0f, 0.5f), new Vector2(6f, 0f), new Vector2(h - 12f, h - 12f), new Vector2(0f, 0.5f));
else Stretch(c.icon.rectTransform, 10f, 10f, 10f, 10f);
if (pill)
{
c.name = NewText(c.root, "Name", PS.chipFontPx - 2f, TextAlignmentOptions.MidlineLeft, TextCol());
var nrt = c.name.rectTransform; nrt.anchorMin = new Vector2(0f, 0f); nrt.anchorMax = new Vector2(1f, 1f); nrt.offsetMin = new Vector2(h - 2f, 2f); nrt.offsetMax = new Vector2(-8f, -2f);
c.name.enableWordWrapping = true; c.name.lineSpacing = -12f;
}
else
{
c.lv = NewText(c.root, "Lv", PS.chipFontPx, TextAlignmentOptions.Bottom, Color.white); c.lv.fontStyle = FontStyles.Bold; ApplyOutline(c.lv);
var lrt = c.lv.rectTransform; lrt.anchorMin = new Vector2(0f, 0f); lrt.anchorMax = new Vector2(1f, 0f); lrt.pivot = new Vector2(0.5f, 1f); lrt.pivot = new Vector2(0.5f, 0.5f); lrt.anchoredPosition = new Vector2(0f, 6f); lrt.sizeDelta = new Vector2(20f, PS.chipFontPx + 6f);
}
var btn = c.ring.gameObject.AddComponent<Button>(); btn.targetGraphic = c.ring;
var cc = c; btn.onClick.AddListener(() => { if (cc.slot != null) OpenSkillDetail(cc.slot); });
return c;
}
// ── 갱신 ─────────────────────────────────────────────────────────
void RefreshPartSkillUi()
{
if (!PsOn) return;
if (WLPartSkillState.Version == 0) WLPartSkillState.Rebuild("gui");
var L = WLPartSkillState.Current;
var names = WLPartSkillData.Stats.partStatNames;
for (int i = 0; i < 6; i++)
{
var type = SlotType(i);
var it = WLInventory.EquippedInSlot(type);
if (_slotNames[i] != null)
{
string stat = it != null ? WLPartStats.ItemStatLine(it, true) : null;
string kind; if (string.IsNullOrEmpty(stat) && names != null && names.TryGetValue(((int)type).ToString(), out kind)) stat = kind;
_slotNames[i].text = WLEquipTables.SlotName(type) + (string.IsNullOrEmpty(stat) ? "" : "\n<size=" + PS.slotStatFontPx.ToString("0") + "><color=#" + ColorUtility.ToHtmlStringRGB(it != null ? Col("statLine", new Color(1f, 0.86f, 0.45f, 1f)) : Col("textDim", Color.gray)) + ">" + stat + "</color></size>");
}
var b = _psBadges[i]; if (b == null) continue;
WLPartSlot s; L.byPart.TryGetValue(type, out s);
b.slot = s != null && s.def != null ? s : null;
SetOn(b.root, b.slot != null);
if (b.slot == null) continue;
b.ring.color = AttrCol(s.def.attr, s.synergy);
b.icon.sprite = WLPartSkillData.Icon(s.def.icon); b.icon.color = b.icon.sprite != null ? Color.white : new Color(1f, 1f, 1f, 0f);
b.lv.text = s.synergy ? "★" : "Lv" + s.level;
}
// 장착 스킬 줄 — 액티브(무기 회전 도끼 · 갑옷 · 목걸이 · 반지 순) · 패시브(장갑 · 신발)
var acts = new List<WLPartSlot>();
foreach (var p in new[] { eFREquipmentType.Weapon, eFREquipmentType.Armor, eFREquipmentType.Necklace, eFREquipmentType.Belt })
{
WLPartSlot s; if (L.byPart.TryGetValue(p, out s) && s.def != null && s.def.kind == WLPartKind.Active) acts.Add(s);
}
foreach (var w in L.weaponSyns) acts.Add(w); // #895 무기 시너지 = 장착 장비 조건(무기 무관 · 대기 포함)
var pas = new List<WLPartSlot>();
foreach (var p in new[] { eFREquipmentType.Glove, eFREquipmentType.Shose })
{
WLPartSlot s; if (L.byPart.TryGetValue(p, out s) && s.def != null && s.def.kind == WLPartKind.Passive) pas.Add(s);
}
int ai = 0, pi = 0;
foreach (var c in _psChips)
{
WLPartSlot s = null;
if (!c.passive) { if (ai < acts.Count) s = acts[ai]; ai++; } else { if (pi < pas.Count) s = pas[pi]; pi++; }
c.slot = s;
bool has = s != null;
c.ring.color = has ? AttrCol(s.def.attr, s.synergy) : Col("frameEmpty", new Color(0.45f, 0.45f, 0.5f, 0.6f));
c.icon.sprite = has ? WLPartSkillData.Icon(s.def.icon) : null;
c.icon.color = c.icon.sprite != null ? Color.white : new Color(1f, 1f, 1f, 0f);
if (c.lv != null) c.lv.text = has ? (s.synergy ? "★" : "Lv" + s.level) : "";
if (c.name != null) c.name.text = has ? s.def.name + "\n<size=80%>Lv" + s.level + " · " + WLPartSkillText.LevelSummary(s.def, s.level, s.mult) + "</size>" : "<color=#" + ColorUtility.ToHtmlStringRGB(Col("textDim", Color.gray)) + ">" + (c == _psChips[4] ? "장갑" : "신발") + " 패시브</color>";
}
// 시너지 알림 — 성립한 것 우선 · 아니면 가장 가까운(짝이 장착된) 것
string line = "";
WLPartSlot best = null; int bestScore = -1;
var cands = new List<WLPartSlot>(acts);
foreach (var s in cands)
{
if (s.def == null || !s.def.HasSynergy) continue;
int score = s.synergy ? 1000 : (s.synPartnerLevel > 0 ? 100 + s.level + s.synPartnerLevel : s.level);
if (score > bestScore) { bestScore = score; best = s; }
}
if (best != null) line = (L.synergyCount > 0 ? "<color=#FFD24A>시너지 " + L.synergyCount + "</color> " : "시너지 ") + WLPartSkillText.SynergyLine(best, true);
else line = "<color=#" + ColorUtility.ToHtmlStringRGB(Col("textDim", Color.gray)) + ">스킬 장비를 끼우면 짝 패시브와의 시너지가 여기 보인다</color>";
_psSyn.text = line;
if (_psDetail != null && _psDetail.activeSelf && _psdCur != null) FillSkillDetail(_psdCur);
}
void OnPsSynergyFound(string key)
{
var d = WLPartSkillData.Def(key);
if (d == null || _message == null || !gameObject.activeInHierarchy) return;
_message.text = "<color=#FFD24A>시너지 발견!</color> [" + (string.IsNullOrEmpty(d.synName) ? d.name : d.synName) + "] · 도감에 기록됨";
}
void ClosePartSkillPopups()
{
if (_psDetail != null) _psDetail.SetActive(false);
if (_psBook != null) _psBook.SetActive(false);
}
/// <summary>아이템 상세 팝업 맨 위 문단 — 이 장비가 주는 스킬 · 지금 등급의 단계 · 시너지 조건.</summary>
string PartSkillPopupText(Item sel)
{
if (!PsOn || sel == null || sel.itemJson == null || !sel.itemJson.IsEquipment) return "";
var d = WLPartSkillData.DefForGroup(sel.itemJson.GroupNo);
if (d == null) return "";
var s = SlotFor(sel, d);
var sb = new System.Text.StringBuilder();
string attr = WLPartSkillText.AttrName(d.attr);
if (d.kind == WLPartKind.WeaponSyn)
sb.Append("<color=#C9A2FF>무기 시너지</color> ").Append(d.name).Append(": ").Append(d.synDesc).Append('\n');
else
sb.Append("<color=#FFD24A>스킬</color> ").Append(d.name).Append(" <size=80%>(").Append(attr).Append(")</size> Lv").Append(s.level)
.Append(s.mult > 1.0001f ? " ×" + s.mult.ToString("0.0#") : "").Append('\n')
.Append("<size=90%>").Append(d.desc).Append("</size>\n<size=85%>").Append(WLPartSkillText.LevelSummary(d, s.level, s.mult)).Append("</size>\n");
string syn = WLPartSkillText.SynergyLine(s, true);
if (!string.IsNullOrEmpty(syn)) sb.Append("<size=85%>").Append(syn).Append("</size>\n");
sb.Append('\n');
return sb.ToString();
}
/// <summary>아이템 1개를 「장착했다면」의 슬롯 상태(시너지 짝 = 지금 장착된 패시브).</summary>
WLPartSlot SlotFor(Item it, WLPartSkillDef d)
{
var L = WLPartSkillState.Current;
WLPartSlot cur;
if (L.byPart.TryGetValue(it.itemJson.EquipmentType, out cur) && cur.item != null && cur.item.rawItem._id == it.rawItem._id) return cur;
var s = new WLPartSlot { part = it.itemJson.EquipmentType, item = it, def = d };
s.tier = WLPartStats.TierOf(it); s.level = WLPartSkillData.TierLevel(s.tier); s.mult = WLPartSkillData.TierMult(s.tier);
int need = WLPartSkillData.S.synergyLevel;
if (!string.IsNullOrEmpty(d.synKey))
{
var p = L.PassiveByKey(d.synKey); s.synPartner = WLPartSkillData.Def(d.synKey); s.synPartnerLevel = p != null ? p.level : 0;
s.synergy = s.level >= need && p != null && p.level >= need;
}
else if (!string.IsNullOrEmpty(d.combineKey))
{
var o = L.ActiveByKey(d.combineKey); s.synPartner = WLPartSkillData.Def(d.combineKey); s.synPartnerLevel = o != null ? o.level : 0;
s.synergy = s.level >= need && o != null && o.level >= need;
}
return s;
}
// ── 스킬 상세 ────────────────────────────────────────────────────
WLPartSlot _psdCur;
void BuildSkillDetail(Sprite circle)
{
_psDetail = new GameObject("SkillDetail", typeof(RectTransform)); _psDetail.layer = UILayer; _psDetail.transform.SetParent(_root, false);
var prt = (RectTransform)_psDetail.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(() => _psDetail.SetActive(false));
var box = NewPopupBox(prt, "Box", new Vector2(PS.detailW, PS.detailH)); var brt = box.rectTransform;
var title = NewRT(brt, "TitleArea"); title.anchorMin = new Vector2(0f, 1f); title.anchorMax = new Vector2(1f, 1f); title.pivot = new Vector2(0.5f, 1f); title.sizeDelta = new Vector2(0f, PS.detailTitleH);
_psdTitle = NewText(title, "Name", L.popupNameFontPx, TextAlignmentOptions.Midline, TextCol()); _psdTitle.fontStyle = FontStyles.Bold;
var trt = _psdTitle.rectTransform; trt.anchorMin = new Vector2(0f, 0.3f); trt.anchorMax = new Vector2(1f, 1f); trt.offsetMin = new Vector2(110f, 0f); trt.offsetMax = new Vector2(-110f, -12f);
_psdSub = NewText(title, "Sub", PS.detailFontPx - 2f, TextAlignmentOptions.Midline, Col("textDim", Color.white));
var surt = _psdSub.rectTransform; surt.anchorMin = new Vector2(0f, 0f); surt.anchorMax = new Vector2(1f, 0.32f); surt.offsetMin = new Vector2(40f, 4f); surt.offsetMax = new Vector2(-40f, 0f);
var close = NewIconButton(title, "Close", "closeIcon", L.closeBtnW, () => _psDetail.SetActive(false), "closeBtnBg");
Place((RectTransform)close.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 = Vector2.zero; body.anchorMax = Vector2.one; body.offsetMin = new Vector2(36f, 36f); body.offsetMax = new Vector2(-36f, -PS.detailTitleH - 6f);
var vl = body.gameObject.AddComponent<VerticalLayoutGroup>(); vl.spacing = 10f;
vl.childControlWidth = true; vl.childControlHeight = true; vl.childForceExpandWidth = true; vl.childForceExpandHeight = false;
// 아이콘 + 설명
var head = NewImage(body, "Head", Spr("innerBox"), Col("innerBox", new Color(0f, 0f, 0f, 0.35f))); Pref(head.rectTransform, 0f, PS.detailIcon + 24f);
_psdRing = NewImage(head.rectTransform, "Ring", circle, Color.white, Image.Type.Simple);
Place(_psdRing.rectTransform, new Vector2(0f, 0.5f), new Vector2(14f, 0f), new Vector2(PS.detailIcon, PS.detailIcon), new Vector2(0f, 0.5f));
var inner = NewImage(_psdRing.rectTransform, "Bg", circle, Col("slotInner", new Color(0.12f, 0.1f, 0.15f, 1f)), Image.Type.Simple); Stretch(inner.rectTransform, 7f, 7f, 7f, 7f);
_psdIcon = NewImage(_psdRing.rectTransform, "Icon", null, Color.white, Image.Type.Simple); _psdIcon.preserveAspect = true; Stretch(_psdIcon.rectTransform, 18f, 18f, 18f, 18f);
_psdDesc = NewText(head.rectTransform, "Desc", PS.detailFontPx, TextAlignmentOptions.MidlineLeft, TextCol()); _psdDesc.enableWordWrapping = true;
var drt = _psdDesc.rectTransform; drt.anchorMin = Vector2.zero; drt.anchorMax = Vector2.one; drt.offsetMin = new Vector2(PS.detailIcon + 34f, 8f); drt.offsetMax = new Vector2(-14f, -8f);
// 등급별 개방 표 7행
for (int t = 1; t <= 7; t++)
{
var row = NewImage(body, "Tier" + t, Spr("innerBox"), Col("innerBox", new Color(0f, 0f, 0f, 0.35f))); Pref(row.rectTransform, 0f, PS.detailRowH);
var tb = NewImage(row.rectTransform, "TierBg", Spr("gradeLabel"), TierCol(t));
Place(tb.rectTransform, new Vector2(0f, 0.5f), new Vector2(8f, 0f), new Vector2(130f, PS.detailRowH - 12f), new Vector2(0f, 0.5f));
var tt = NewText(tb.rectTransform, "Tier", PS.detailFontPx - 2f, TextAlignmentOptions.Midline, Color.white); tt.fontStyle = FontStyles.Bold; Stretch(tt.rectTransform); ApplyOutline(tt);
var tx = NewText(row.rectTransform, "Text", PS.detailFontPx - 2f, TextAlignmentOptions.MidlineLeft, TextCol()); tx.enableWordWrapping = false; tx.overflowMode = TextOverflowModes.Ellipsis;
var xrt = tx.rectTransform; xrt.anchorMin = Vector2.zero; xrt.anchorMax = Vector2.one; xrt.offsetMin = new Vector2(152f, 0f); xrt.offsetMax = new Vector2(-12f, 0f);
_psdRowBg.Add(row); _psdRowTierBg.Add(tb); _psdRowTier.Add(tt); _psdRowText.Add(tx);
}
var synBox = NewImage(body, "Synergy", Spr("innerBox"), Col("synergyBox", new Color(0.35f, 0.28f, 0.08f, 0.55f)));
var sle = synBox.gameObject.AddComponent<LayoutElement>(); sle.flexibleHeight = 1f; sle.minHeight = 150f;
_psdSyn = NewText(synBox.rectTransform, "Text", PS.detailFontPx - 1f, TextAlignmentOptions.TopLeft, TextCol()); _psdSyn.enableWordWrapping = true;
Stretch(_psdSyn.rectTransform, PS.detailBoxPad, PS.detailBoxPad, PS.detailBoxPad, PS.detailBoxPad);
var runeBox = NewImage(body, "Rune", Spr("innerBox"), Col("innerBox", new Color(0f, 0f, 0f, 0.35f))); Pref(runeBox.rectTransform, 0f, 92f);
_psdRune = NewText(runeBox.rectTransform, "Text", PS.detailFontPx - 4f, TextAlignmentOptions.MidlineLeft, Col("textDim", Color.white)); _psdRune.enableWordWrapping = true;
Stretch(_psdRune.rectTransform, PS.detailBoxPad, 6f, PS.detailBoxPad, 6f);
_psDetail.SetActive(false);
}
void OpenSkillDetail(WLPartSlot s)
{
if (!PsOn || s == null || s.def == null || _psDetail == null) return;
_psdCur = s;
FillSkillDetail(s);
_psDetail.SetActive(true);
_psDetail.transform.SetAsLastSibling();
PartSkillDetailOpens++;
}
void FillSkillDetail(WLPartSlot s)
{
var d = s.def;
string itemName = s.item != null ? s.item.itemJson.Item_name : "";
_psdTitle.text = (s.synergy && !string.IsNullOrEmpty(d.synName) ? d.synName : d.name);
_psdSub.text = WLPartSkillText.PartName(d.part) + " · " + WLPartSkillText.AttrName(d.attr) + (itemName.Length > 0 ? " · " + itemName : "") +
" · " + WLEquipGrades.TierLabel(Mathf.Max(1, s.tier), true) + " = Lv" + s.level + (s.synergy ? " <color=#FFD24A>★ 시너지</color>" : "");
_psdRing.color = AttrCol(d.attr, s.synergy);
_psdIcon.sprite = WLPartSkillData.Icon(d.icon); _psdIcon.color = _psdIcon.sprite != null ? Color.white : new Color(1f, 1f, 1f, 0f);
_psdDesc.text = (d.kind == WLPartKind.WeaponSyn ? d.synDesc : d.desc) + (d.kind == WLPartKind.WeaponSyn ? "\n<size=85%><color=#" + ColorUtility.ToHtmlStringRGB(Col("textDim", Color.gray)) + ">장착한 장비 조건(5단계)이면 어떤 무기든 기본 공격에 붙는다(#895)</color></size>" : "");
for (int t = 1; t <= 7 && t - 1 < _psdRowText.Count; t++)
{
int i = t - 1;
var tier = WLEquipGrades.Tier(t);
_psdRowTier[i].text = tier != null ? tier.name : ("등급" + t);
_psdRowTierBg[i].color = TierCol(t);
_psdRowText[i].text = d.kind == WLPartKind.WeaponSyn ? (t >= WLPartSkillData.S.synergyLevel ? "조건 장비 " + WLPartSkillData.S.synergyLevel + "단계면 시너지 개방(무기 무관)" : "-") : WLPartSkillText.TierRow(d, t);
bool cur = t == s.tier;
_psdRowBg[i].color = cur ? Col("rowOn", new Color(1f, 0.92f, 0.55f, 0.4f)) : Col("innerBox", new Color(0f, 0f, 0f, 0.35f));
}
// 시너지
var sb = new System.Text.StringBuilder();
if (d.HasSynergy)
{
sb.Append("<color=#FFD24A><b>시너지: ").Append(string.IsNullOrEmpty(d.synName) ? d.name : d.synName).Append("</b></color>\n");
var p = d.HasSynergy ? WLPartSkillData.Def(!string.IsNullOrEmpty(d.synKey) ? d.synKey : d.combineKey) : null;
int need = WLPartSkillData.S.synergyLevel;
if (d.kind == WLPartKind.WeaponSyn)
{ // #895 무기와 무관 — 장착 장비 조건만
var q = string.IsNullOrEmpty(d.reqKey) ? null : WLPartSkillData.Def(d.reqKey);
sb.Append("조건: ").Append(p != null ? p.name + "(" + WLPartSkillText.PartName(p.part) + ") " : "? ").Append(need).Append("단계")
.Append(q != null ? " + " + q.name + "(" + WLPartSkillText.PartName(q.part) + ") " + need + "단계" : "").Append(" · 무기 무관\n");
}
else
sb.Append("조건: ").Append(WLPartSkillText.PartName(d.part)).Append(' ').Append(need).Append("단계 + ")
.Append(p != null ? p.name + "(" + WLPartSkillText.PartName(p.part) + ") " : "? ").Append(need).Append("단계\n");
sb.Append("효과: ").Append(d.synDesc).Append('\n');
if (d.kind != WLPartKind.Passive && d.kind != WLPartKind.WeaponSyn && d.synLevel > 0 && string.IsNullOrEmpty(d.combineKey))
sb.Append("<size=88%>시너지 수치: ").Append(WLPartSkillText.LevelSummary(d, d.synLevel, 1f)).Append("</size>\n");
sb.Append("지금: ").Append(WLPartSkillText.SynergyLine(s, true));
}
else if (d.kind == WLPartKind.Passive)
{
sb.Append("<color=#7CE08A><b>짝 스킬</b></color>\n");
int n = 0;
foreach (var a in WLPartSkillData.Defs)
if (a.synKey == d.key || (!string.IsNullOrEmpty(a.reqKey) && a.reqKey == d.key))
{
sb.Append(n++ > 0 ? " · " : "");
if (a.kind == WLPartKind.WeaponSyn) sb.Append("무기 시너지 ").Append(a.synName).Append("(무기 무관)"); // #895
else sb.Append(a.name).Append('(').Append(WLPartSkillText.PartName(a.part)).Append(") = ").Append(a.synName);
}
if (n == 0) sb.Append("없음");
sb.Append("\n<size=88%>짝 스킬과 함께 둘 다 5단계면 시너지</size>");
}
else sb.Append("<color=#" + ColorUtility.ToHtmlStringRGB(Col("textDim", Color.gray)) + ">시너지 없음 · 등급을 올리면 수치가 강해진다</color>");
_psdSyn.text = sb.ToString();
_psdRune.text = "룬 칸(이후) · FR 엘리트 · 레전드 룬 = 엑스트라 버프 자리";
}
// ── 시너지 도감 ──────────────────────────────────────────────────
void BuildSynergyBook()
{
_psBook = new GameObject("SynergyBook", typeof(RectTransform)); _psBook.layer = UILayer; _psBook.transform.SetParent(_root, false);
var prt = (RectTransform)_psBook.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(() => _psBook.SetActive(false));
var box = NewPopupBox(prt, "Box", new Vector2(PS.bookW, PS.bookH)); var brt = box.rectTransform;
var title = NewRT(brt, "TitleArea"); title.anchorMin = new Vector2(0f, 1f); title.anchorMax = new Vector2(1f, 1f); title.pivot = new Vector2(0.5f, 1f); title.sizeDelta = new Vector2(0f, L.popupTitleH);
var tt = NewText(title, "Name", L.popupNameFontPx, TextAlignmentOptions.Midline, TextCol()); tt.fontStyle = FontStyles.Bold; tt.text = "시너지 도감";
Stretch(tt.rectTransform, 110f, 10f, 110f, 10f);
var close = NewIconButton(title, "Close", "closeIcon", L.closeBtnW, () => _psBook.SetActive(false), "closeBtnBg");
Place((RectTransform)close.transform, new Vector2(1f, 1f), new Vector2(-14f, -14f), new Vector2(L.closeBtnW, L.closeBtnH), new Vector2(1f, 1f));
_psBookHead = NewText(brt, "Head", PS.bookFontPx, TextAlignmentOptions.MidlineLeft, Col("textDim", Color.white));
var hrt = _psBookHead.rectTransform; hrt.anchorMin = new Vector2(0f, 1f); hrt.anchorMax = new Vector2(1f, 1f); hrt.pivot = new Vector2(0.5f, 1f);
hrt.anchoredPosition = new Vector2(0f, -L.popupTitleH); hrt.sizeDelta = new Vector2(-72f, 50f);
// 스크롤 목록
var listGo = new GameObject("List", typeof(RectTransform), typeof(Image), typeof(ScrollRect)); listGo.layer = UILayer; listGo.transform.SetParent(brt, false);
var lrt = (RectTransform)listGo.transform; lrt.anchorMin = Vector2.zero; lrt.anchorMax = Vector2.one; lrt.offsetMin = new Vector2(30f, 34f); lrt.offsetMax = new Vector2(-30f, -L.popupTitleH - 56f);
listGo.GetComponent<Image>().color = new Color(0f, 0f, 0f, 0.01f);
var vp = new GameObject("Viewport", typeof(RectTransform), typeof(Image), typeof(RectMask2D)); vp.layer = UILayer; vp.transform.SetParent(listGo.transform, false);
var vrt = (RectTransform)vp.transform; Stretch(vrt); vp.GetComponent<Image>().color = new Color(0f, 0f, 0f, 0.01f);
var cgo = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter)); cgo.layer = UILayer; cgo.transform.SetParent(vp.transform, false);
_psBookContent = (RectTransform)cgo.transform; _psBookContent.anchorMin = new Vector2(0f, 1f); _psBookContent.anchorMax = new Vector2(1f, 1f); _psBookContent.pivot = new Vector2(0.5f, 1f); _psBookContent.offsetMin = _psBookContent.offsetMax = Vector2.zero;
var vl = cgo.GetComponent<VerticalLayoutGroup>(); vl.spacing = 10f; vl.childControlWidth = true; vl.childControlHeight = true; vl.childForceExpandWidth = true; vl.childForceExpandHeight = false;
cgo.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var sr = listGo.GetComponent<ScrollRect>(); sr.content = _psBookContent; sr.viewport = vrt; sr.horizontal = false; sr.vertical = true; sr.movementType = ScrollRect.MovementType.Clamped; sr.scrollSensitivity = 40f;
// 행 = 시너지 짝이 있는 액티브 · 무기 시너지 · 결합(정령왕) — 아이템이 배정된 것만(배정 0 = 얻을 수 없음)
foreach (var d in WLPartSkillData.Defs)
{
bool syn = (d.kind == WLPartKind.Active || d.kind == WLPartKind.WeaponSyn) && !string.IsNullOrEmpty(d.synKey);
bool comb = d.kind == WLPartKind.Combine;
if (!syn && !comb) continue;
if (syn && WLPartSkillData.GroupsOf(d.kind == WLPartKind.WeaponSyn ? d.synKey : d.key).Count == 0) continue; // #895 무기 시너지 = 짝 장비가 있으면
var row = NewImage(_psBookContent, "Row_" + d.key, Spr("innerBox"), Col("innerBox", new Color(0f, 0f, 0f, 0.35f))); Pref(row.rectTransform, 0f, PS.bookRowH);
var tx = NewText(row.rectTransform, "Text", PS.bookFontPx, TextAlignmentOptions.MidlineLeft, TextCol()); tx.enableWordWrapping = true;
Stretch(tx.rectTransform, 20f, 6f, 20f, 6f);
_psBookRowBg.Add(row); _psBookRows.Add(tx); _psBookKeys.Add(comb ? "spirit_king" : d.key);
}
_psBook.SetActive(false);
}
void OpenSynergyBook()
{
if (!PsOn || _psBook == null) return;
int found = 0, total = _psBookKeys.Count;
for (int i = 0; i < _psBookKeys.Count; i++)
{
var key = _psBookKeys[i];
bool f = WLPartSkillState.SeenSynergies.Contains(key);
if (f) found++;
string line;
if (key == "spirit_king")
{
var fire = WLPartSkillData.Def("fire_spirit"); var water = WLPartSkillData.Def("water_spirit");
line = "<color=#FF9A9A>결합</color> " + (fire != null ? fire.name + "(" + WLPartSkillText.PartName(fire.part) + ")" : "?") + " + " +
(f ? (water != null ? water.name + "(" + WLPartSkillText.PartName(water.part) + ")" : "?") : "???") + "\n= " + (f ? "<color=#FFD24A>정령왕</color>" : "???");
}
else
{
var d = WLPartSkillData.Def(key); var p = d != null ? WLPartSkillData.Def(d.synKey) : null;
if (d != null && d.kind == WLPartKind.WeaponSyn)
{ // #895 무기와 무관 — 조건 장비를 힌트로 보인다
var q = string.IsNullOrEmpty(d.reqKey) ? null : WLPartSkillData.Def(d.reqKey);
line = "<color=#C9A2FF>무기 시너지</color> " + (p != null ? p.name + "(" + WLPartSkillText.PartName(p.part) + ")" : "?") +
(q != null ? " + " + q.name + "(" + WLPartSkillText.PartName(q.part) + ")" : "") + "\n= " + (f ? "<color=#FFD24A>" + d.synName + "</color>" : "???");
_psBookRows[i].text = line + (f ? " <color=#7CE08A>★ 찾음</color>" : "");
_psBookRowBg[i].color = f ? Col("rowOn", new Color(1f, 0.92f, 0.55f, 0.4f)) : Col("innerBox", new Color(0f, 0f, 0f, 0.35f));
continue;
}
string a = d == null ? "?" : d.name + "(" + WLPartSkillText.PartName(d.part) + ")";
line = a + " + " + (f ? (p != null ? p.name + "(" + WLPartSkillText.PartName(p.part) + ")" : "?") : "???") + "\n= " + (f ? "<color=#FFD24A>" + (d != null ? d.synName : "?") + "</color>" : "???");
}
_psBookRows[i].text = line + (f ? " <color=#7CE08A>★ 찾음</color>" : "");
_psBookRowBg[i].color = f ? Col("rowOn", new Color(1f, 0.92f, 0.55f, 0.4f)) : Col("innerBox", new Color(0f, 0f, 0f, 0.35f));
}
_psBookHead.text = "시너지 · 결합 " + total + " · 찾은 것 " + found;
_psBook.SetActive(true);
_psBook.transform.SetAsLastSibling();
PartSkillBookOpens++;
}
// 프로브용
public void OpenSkillDetailForProbe(int slotIndex) { var b = slotIndex >= 0 && slotIndex < 6 ? _psBadges[slotIndex] : null; if (b != null && b.slot != null) OpenSkillDetail(b.slot); }
public void OpenSynergyBookForProbe() { OpenSynergyBook(); }
public void ClosePartSkillForProbe() { ClosePartSkillPopups(); }
public string PartSkillUiStateForProbe
{
get
{
int badges = 0; foreach (var b in _psBadges) if (b != null && b.root.gameObject.activeSelf) badges++;
int chips = 0; foreach (var c in _psChips) if (c.slot != null) chips++;
return "on=" + PsOn + " badges=" + badges + " chips=" + chips + " syn='" + (_psSyn != null ? _psSyn.text : "") + "' detail=" + (_psDetail != null && _psDetail.activeSelf) + " book=" + (_psBook != null && _psBook.activeSelf) + " rows=" + _psBookRows.Count;
}
}
}
}