509 lines
38 KiB
C#
509 lines
38 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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;
|
||
}
|
||
}
|
||
}
|
||
}
|