2026-10-18 15:09:05 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// 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 ) ;
2026-10-22 15:24:35 +00:00
// 등급별 개방 표(등급표 단계 수 · #973 G1~G8)
for ( int t = 1 ; t < = Mathf . Max ( 1 , WLEquipGrades . MaxTier ) ; t + + )
2026-10-18 15:09:05 +00:00
{
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>" : "" ) ;
2026-10-22 15:24:35 +00:00
for ( int t = 1 ; t < = WLEquipGrades . MaxTier & & t - 1 < _psdRowText . Count ; t + + )
2026-10-18 15:09:05 +00:00
{
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 ;
}
}
}
}