2026-11-05 12:11:10 +00:00
using System.Collections.Generic ;
using UnityEngine ;
using UnityEngine.UI ;
namespace TKS.OutGame
{
2026-11-09 03:20:32 +00:00
/// <summary>
/// 영웅·병사 카드(가로형 · UI 상업용 재설계 ② · 설계서 §3② · 팩 CardFrame01 5층 등급색) — 초상(뒤 빛) · 병종 원 · Lv 배지 · 세력 깃발 · 출전 꼬리 칩 ·
/// 아래 이름 띠(이름 · ★ · 등급 칩) · 미보유 = 빈 카드(문양 + 초상 어둡게). 목록 · 편성 보유 행 · 도감 공용.
/// </summary>
2026-11-05 12:11:10 +00:00
public static class HeroCards
{
public static Image Card ( Transform content , OutGameSession s , HeroDb . Hero h , bool deployed , bool owned , System . Action onTap )
{
2026-11-09 03:20:32 +00:00
var cell = UiKit . Cell ( content , "Card_" + h . id , Color . clear , onTap ) ;
cell . sprite = null ; // 바닥 = 클릭 판(투명) · 그림은 층으로
2026-11-05 12:11:10 +00:00
var lv = s . Save . Level ( h . id ) ;
var hs = s . Save . Hero ( h . id ) ;
2026-11-09 03:20:32 +00:00
string g = h . isSoldier | | hs = = null | | string . IsNullOrEmpty ( hs . grade ) ? "white" : hs . grade ;
string G = "grade_" + g + "_" ;
var t = cell . transform ;
if ( owned )
{
UiKit . Layer ( t , "Bg" , "card_bg" , UiKit . Col ( G + "card_bg" , UiKit . Paper ) ) ;
UiKit . Layer ( t , "Grad" , "card_grad" , UiKit . Col ( G + "card_grad" , UiKit . White ) , 0f , 6f , 0f , 0f ) ;
var glow = UiKit . Skin ( UiKit . Rect ( "Glow" , t , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 72f , 6f ) , new Vector2 ( 190f , 190f ) ) , "card_glow" , UiKit . Col ( G + "card_glow" , UiKit . White ) ) ;
glow . enabled = UISkin . Has ( "card_glow" ) ;
}
else
{
UiKit . Layer ( t , "Bg" , "card_empty" , UiKit . Col ( "card_empty" , UiKit . Slot ) ) ;
var pat = UiKit . Layer ( t , "Pattern" , "card_empty_pattern" , UiKit . Col ( "card_empty_pattern" , UiKit . Slot ) , 6f , 6f , 6f , 6f ) ;
pat . type = Image . Type . Tiled ;
}
UiKit . Portrait ( "Face" , t , h , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 12f , 6f ) , new Vector2 ( 120f , 116f ) , false , 2f , Color . clear ) ;
if ( ! owned ) UiKit . Paint ( UiKit . Rect ( "Dim" , t , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 12f , 6f ) , new Vector2 ( 120f , 116f ) ) , UiKit . Col ( "card_dim" , UiKit . Dark ) , false ) ;
var band = UiKit . Rect ( "Band" , t , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , Vector2 . zero , new Vector2 ( 0f , 62f ) ) ;
band . anchorMin = new Vector2 ( 0f , 0f ) ;
band . anchorMax = new Vector2 ( 1f , 0f ) ;
band . sizeDelta = new Vector2 ( 0f , 62f ) ;
UiKit . Skin ( band , "card_bottom" , UiKit . Col ( "card_bottom" , UiKit . Dark ) ) . enabled = UISkin . Has ( "card_bottom" ) ;
UiKit . Layer ( t , "Border" , "card_border" , UiKit . Col ( owned ? "card_border" : "card_empty_border" , UiKit . Line ) ) ;
2026-11-05 12:11:10 +00:00
int star = hs ! = null ? hs . star : h . rarity ;
2026-11-09 03:20:32 +00:00
UiKit . StarRow ( t , star , star , new Vector2 ( 0f , 0f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 16f , 8f ) , 24f ) ;
var name = UiKit . Title ( UiKit . Label ( t , h . nameKo , 40 , owned ? UiKit . White : UiKit . InkSoft , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 142f , 4f ) , new Vector2 ( 260f , 56f ) ) , true ) ;
name . horizontalOverflow = HorizontalWrapMode . Overflow ;
// 위 줄: 병종 원 · Lv 배지 · 세력 깃발 · (출전 꼬리 칩)
2026-11-09 10:59:56 +00:00
// 병과 = 단색 심볼만(배경 틀 없음 · PD 2026-11-09)
var ring = UiKit . Rect ( "Type" , t , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 142f , - 12f ) , new Vector2 ( 52f , 52f ) ) ;
var cls = UiKit . Icon ( ring , UiKit . TypeSymbolId ( h . type ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , 50f ) ;
2026-11-09 11:20:15 +00:00
if ( cls = = null ) UiKit . Title ( UiKit . FillLabel ( ring , UnitTypeInfo . Mark ( h . type ) , 26 , UiKit . White ) , true ) ;
2026-11-09 03:20:32 +00:00
if ( owned )
{
var badge = UiKit . Icon ( t , "lv_badge_s" , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 202f , - 8f ) , 56f ) ;
var lt = UiKit . Title ( UiKit . Label ( badge ! = null ? badge . transform : t , ( lv ! = null ? lv . lv : 1 ) . ToString ( ) , 26 , UiKit . White , TextAnchor . MiddleCenter ,
badge ! = null ? new Vector2 ( 0.5f , 0.5f ) : new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , badge ! = null ? new Vector2 ( 0f , 2f ) : new Vector2 ( 230f , - 36f ) , new Vector2 ( 56f , 40f ) ) , true ) ;
lt . horizontalOverflow = HorizontalWrapMode . Overflow ;
}
2026-11-09 10:59:56 +00:00
UiKit . DropShadow ( UiKit . FactionBadge ( t , h . faction , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 264f , - 12f ) , 46f ) , 2f ) ; // 진영 원 배지(PD 2026-11-09)
2026-11-05 12:11:10 +00:00
if ( deployed )
{
2026-11-09 03:20:32 +00:00
var tail = UiKit . Skin ( UiKit . Rect ( "Deployed" , t , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( - 10f , - 10f ) , new Vector2 ( 104f , 46f ) ) , "tail" , UiKit . Col ( "tail_red" , UiKit . Red ) ) ;
UiKit . Title ( UiKit . FillLabel ( tail . transform , "출전" , 26 , UiKit . White ) , true ) ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 03:20:32 +00:00
if ( owned & & hs ! = null & & ! h . isSoldier & & UISkin . Has ( "para_" + g ) )
2026-11-05 12:11:10 +00:00
{
2026-11-09 03:20:32 +00:00
var chip = UiKit . Skin ( UiKit . Rect ( "Grade" , t , new Vector2 ( 1f , 0f ) , new Vector2 ( 1f , 0f ) , new Vector2 ( - 14f , 11f ) , new Vector2 ( 66f , 40f ) ) , "para_" + g , UiKit . Col ( G + "chip" , UiKit . White ) ) ;
UiKit . Title ( UiKit . FillLabel ( chip . transform , UiKit . GradeKo ( g ) , 24 , UiKit . White ) , true ) ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 03:20:32 +00:00
if ( owned & & s . CanGrowAny ( h . id ) & & UiKit . Icon ( t , "new_dot" , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 14f , - 10f ) , 34f ) = = null )
UiKit . Dot ( "New" , t , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 14f , - 10f ) , 26f , UiKit . Red ) ;
2026-11-05 12:11:10 +00:00
return cell ;
}
}
2026-11-09 03:44:06 +00:00
/// <summary>
/// 영웅 목록 세로 카드(PD 2026-11-09 레퍼런스 = 팩 데모 Collection_List · 칸 320× 431) — 등급색 바탕(마스크) · 그라데이션 · 일러 뒤 빛 · 일러 · 아래 어두운 띠 · 테두리 ·
/// 이름(왼쪽 위) · 병종 원 · 레벨 배지(왼쪽 아래 · 최고 = 노랑) · ★(오른쪽 아래) · 미보유 = 빈 카드(문양 + 얼굴 실루엣). 수치 = 팩 프리팹 RectTransform 그대로.
/// </summary>
public static class CollectionCards
{
public static readonly Vector2 CellSize = new Vector2 ( 320f , 431f ) ;
public static readonly Vector2 Spacing = new Vector2 ( 22f , 20f ) ;
2026-11-09 10:23:50 +00:00
/// <summary>크기 표 size_px(머리 크기 기준 표준 키)가 카드에서 차지할 px — 배율 = UnitPx ÷ size_px(모든 유닛 머리 크기 같음) · 머리 끝 위치(카드 위에서 px).</summary>
2026-11-09 10:30:58 +00:00
const float UnitPx = 360f ; // 450 → 360 = 유닛 20 % 작게(PD 2026-11-09 「현재 비율보다 20 % 정도 작게」)
2026-11-09 04:22:34 +00:00
const float HeadTop = 50f ;
2026-11-09 10:59:56 +00:00
/// <summary>걷는 유닛(보병·창병·궁병·책사) = 머리 끝을 더 내려 발이 카드 아래로 숨게(PD 2026-11-09).</summary>
const float FootDrop = 36f ;
2026-11-09 11:10:50 +00:00
/// <summary>말 탄 유닛(크기 표 mounted) = 걷는 유닛보다 작게 · 말 얼굴이 카드 오른쪽 경계에 닿을 만큼(PD 2026-11-09 0.72 → 「조금 더 크게 · 내려도」).</summary>
const float MountedScale = 0.85f ;
/// <summary>말 탄 유닛 머리 끝 내림(카드 px · PD 2026-11-09).</summary>
const float MountedDrop = 24f ;
2026-11-09 10:59:56 +00:00
/// <summary>말 탄 유닛 전신(발 ~ 머리 끝) 높이 상한(카드 px) — 크기 표 실측이 튀는 그림(예 여포 size_px 449)도 다른 기병과 비슷하게.</summary>
2026-11-09 11:10:50 +00:00
const float MountedMaxBody = 460f ;
2026-11-10 04:13:46 +00:00
/// <summary>눈 줄(카드 위에서 px · 크기 표 eye_up_px 가 있는 유닛) — 머리 끝 맞춤이면 관·투구가 낮은 유닛(전위·태사자)의 얼굴이 위로 뜸 →
/// 눈 높이를 맞춘다(PD 2026-11-10 「너무 위쪽에 등장하는 영웅은 내려」) · 값 = PD 확인 기존 20종의 머리 끝 맞춤 때 눈 높이 중앙값(걷는 216.5 · 기병 210.3).</summary>
const float EyeLine = 216f ;
const float EyeLineMounted = 210f ;
2026-11-09 03:44:06 +00:00
2026-11-10 01:49:27 +00:00
/// <param name="owned">false = 미보유(PD 2026-11-10 「유닛 목록에 장수들의 모습이 보이도록」) — 일러를 테마 card_locked_art 로 조금 어둡게 · 「미보유」 꼬리 칩 · 레벨 배지·성장 점 없음</param>
public static Image Card ( Transform content , OutGameSession s , HeroDb . Hero h , bool deployed , System . Action onTap , bool owned = true )
2026-11-09 03:44:06 +00:00
{
var cell = UiKit . Cell ( content , "Card_" + h . id , Color . clear , onTap ) ;
cell . sprite = null ; // 바닥 = 클릭 판(투명) · 그림은 층으로
var t = cell . transform ;
var lv = s . Save . Level ( h . id ) ;
var hs = s . Save . Hero ( h . id ) ;
string g = h . isSoldier | | hs = = null | | string . IsNullOrEmpty ( hs . grade ) ? "white" : hs . grade ;
string G = "grade_" + g + "_" ;
// 바탕(마스크) 안 = 그라데이션 · 빛 · 일러 · 아래 띠(카드 둥근 모양으로 잘림)
var bg = UiKit . Layer ( t , "Bg" , "card_bg" , UiKit . Col ( G + "card_bg" , UiKit . Paper ) , 2f , 2f , 2f , 2f ) ;
bg . gameObject . AddComponent < Mask > ( ) . showMaskGraphic = true ;
var b = bg . transform ;
UiKit . Layer ( b , "Gradient" , "card_grad" , UiKit . Col ( G + "card_grad" , UiKit . White ) , 0f , 0f , 0f , 44.6f ) ;
UiKit . Skin ( UiKit . Rect ( "Glow" , b , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , new Vector2 ( 226f , 226f ) ) , "card_glow" , UiKit . Col ( G + "card_glow" , UiKit . White ) ) . enabled = UISkin . Has ( "card_glow" ) ;
var sp = UiKit . ArtSprite ( h . art ) ;
if ( sp ! = null )
{
2026-11-09 10:23:50 +00:00
// 머리 크기 통일(PD 2026-11-09 「캐릭터 크기가 동일하게 보이도록」) = 배율 UnitPx ÷ 크기 표 size_px(눈 간격 기준 · 말 탄 유닛은 탄 사람 기준) ·
// 머리 끝(발 + bodyHeightPx · 무기 제외)을 카드 위 HeadTop 에 맞춤 → 다리·말은 아래 띠 뒤로 잘림(레퍼런스 흉상 느낌) · 메타가 없으면 칸 맞춤
2026-11-09 04:22:34 +00:00
var a = UnitArt . Load ( h . art ) ;
var m = a ! = null ? a . Meta : null ;
2026-11-09 10:23:50 +00:00
float body = a ! = null ? a . BodyPx : 0f ;
float size = a ! = null & & a . SizePx > 0.5f ? a . SizePx : body ;
2026-11-09 04:22:34 +00:00
Image art ;
if ( body > 0.5f & & m . pivot ! = null & & m . pivot . Length > 1 )
{
float hgt = sp . rect . height ;
2026-11-09 10:59:56 +00:00
bool mounted = a . Mounted ;
float k = UnitPx / size * ( mounted ? MountedScale : 1f ) ;
if ( mounted ) k = Mathf . Min ( k , MountedMaxBody / body ) ;
2026-11-10 04:13:46 +00:00
float pivotY , y ;
if ( a . EyeUpPx > 0.5f )
{
pivotY = Mathf . Clamp01 ( ( m . pivot [ 1 ] * hgt + a . EyeUpPx ) / hgt ) ; // 눈 줄 = 프레임 아래에서의 비율
y = mounted ? EyeLineMounted : EyeLine ;
}
else
{
pivotY = Mathf . Clamp01 ( ( m . pivot [ 1 ] * hgt + body ) / hgt ) ; // 머리 끝 = 프레임 아래에서의 비율(눈 높이 없는 그림)
y = HeadTop + ( mounted ? MountedDrop : FootDrop ) ;
}
art = UiKit . Rect ( "Character" , b , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , pivotY ) , new Vector2 ( 0f , - y ) , sp . rect . size * k ) . gameObject . AddComponent < Image > ( ) ;
2026-11-09 04:22:34 +00:00
}
else
{
2026-11-09 10:30:58 +00:00
art = UiKit . Rect ( "Character" , b , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - 56f ) , new Vector2 ( 376f , 368f ) ) . gameObject . AddComponent < Image > ( ) ;
2026-11-09 04:22:34 +00:00
art . preserveAspect = true ;
}
2026-11-09 03:44:06 +00:00
art . sprite = sp ;
art . raycastTarget = false ;
2026-11-10 01:49:27 +00:00
if ( ! owned ) art . color = UiKit . Col ( "card_locked_art" , UiKit . InkSoft ) ;
2026-11-09 03:44:06 +00:00
}
else
{
var dot = UiKit . Dot ( "Shape" , b , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - 10f ) , 200f , UiKit . TypeColor ( h . type ) ) ;
UiKit . Title ( UiKit . FillLabel ( dot . transform , UnitTypeInfo . Mark ( h . type ) , 100 , UiKit . White ) , true ) ;
}
var bottom = UiKit . Rect ( "Bottom" , b , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , 21.7f ) , new Vector2 ( 0f , 91.5f ) ) ;
bottom . anchorMin = new Vector2 ( 0f , 0f ) ;
bottom . anchorMax = new Vector2 ( 1f , 0f ) ;
bottom . sizeDelta = new Vector2 ( 52.6f , 91.5f ) ;
UiKit . Skin ( bottom , "card_bottom" , UiKit . Col ( "card_bottom" , UiKit . Dark ) ) . enabled = UISkin . Has ( "card_bottom" ) ;
UiKit . Layer ( t , "Border" , "card_border" , UiKit . Col ( "card_border" , UiKit . Line ) ) ;
// ★(오른쪽 아래 · 42× 45 · 28 간격 겹침)
int star = hs ! = null ? hs . star : h . rarity ;
for ( int i = star - 1 ; i > = 0 ; i - - )
UiKit . Icon ( t , "star_on" , new Vector2 ( 1f , 0f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( - 15.5f - i * 28f , 64.8f ) , 44f ) ;
2026-11-10 01:49:27 +00:00
// 레벨 배지(왼쪽 아래 · 최고 레벨 = 노랑 · 미보유 = 없음)
2026-11-09 03:44:06 +00:00
int lvn = lv ! = null ? lv . lv : 1 ;
bool maxLv = s . Db . Levels ! = null & & lvn > = s . Db . Levels . MaxLevel ;
2026-11-10 01:49:27 +00:00
var badge = owned ? UiKit . Icon ( t , maxLv ? "lv_badge_max" : "lv_badge_s" , new Vector2 ( 0f , 0f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 33.9f , 36.7f ) , 79f ) : null ;
2026-11-09 03:44:06 +00:00
if ( badge ! = null )
{
var bt = UiKit . Title ( UiKit . Label ( badge . transform , lvn . ToString ( ) , 30 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 1f , - 3f ) , new Vector2 ( 74f , 50f ) ) , true ) ;
bt . horizontalOverflow = HorizontalWrapMode . Overflow ;
}
2026-11-09 04:56:07 +00:00
// PD 커스터마이징(2026-11-09): 왼쪽 위 = 진영 깃발 · 그 오른쪽 = 이름(45 · 긴 이름 자동 축소) · 깃발 아래(왼쪽 가장자리) = 출전 · 오른쪽 위 = 병종 원(#35374E 58× 60)
2026-11-09 10:30:58 +00:00
// 진영 = 원 배지(위·촉·오·군 · PD 2026-11-09 · 구 세력 깃발 대체)
UiKit . DropShadow ( UiKit . FactionBadge ( t , h . faction , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 44f , - 42f ) , 62f ) , 3f ) ;
2026-11-09 04:22:34 +00:00
var name = UiKit . Title ( UiKit . Label ( t , h . nameKo , 45 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 80f , - 40.5f ) , new Vector2 ( 172f , 66f ) ) , true ) ;
name . resizeTextForBestFit = true ;
name . resizeTextMinSize = 26 ;
name . resizeTextMaxSize = 45 ;
2026-11-10 01:49:27 +00:00
if ( deployed | | ! owned )
2026-11-09 04:22:34 +00:00
{
2026-11-10 01:49:27 +00:00
var tail = UiKit . Skin ( UiKit . Rect ( owned ? "Deployed" : "Locked" , t , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 10f , - 96f ) , new Vector2 ( owned ? 84f : 104f , 38f ) ) , "tail" ,
owned ? UiKit . Col ( "tail_red" , UiKit . Red ) : UiKit . Col ( "tail_locked" , UiKit . Dark ) ) ;
UiKit . Title ( UiKit . FillLabel ( tail . transform , owned ? "출전" : "미보유" , 24 , UiKit . White ) , true ) ;
2026-11-09 04:22:34 +00:00
}
2026-11-09 10:59:56 +00:00
// 병과 = 단색 심볼만(배경 틀 없음 · PD 2026-11-09 「심볼은 배경 프레임 없이 단색」)
var role = UiKit . Rect ( "Role" , t , new Vector2 ( 1f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( - 42f , - 42f ) , new Vector2 ( 60f , 60f ) ) ;
var sym = UiKit . Icon ( role , UiKit . TypeSymbolId ( h . type ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , 60f ) ;
2026-11-09 11:20:15 +00:00
if ( sym = = null ) UiKit . Title ( UiKit . FillLabel ( role , UnitTypeInfo . Mark ( h . type ) , 28 , UiKit . White ) , true ) ;
2026-11-10 01:49:27 +00:00
if ( owned & & s . CanGrowAny ( h . id ) ) UiKit . Icon ( t , "new_dot" , new Vector2 ( 0f , 0f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 64f , 70f ) , 30f ) ; // 성장 가능 = 레벨 배지 오른쪽 위
2026-11-09 03:44:06 +00:00
return cell ;
}
2026-11-10 01:49:27 +00:00
/// <summary>원래 크기(320× 431) 카드를 holder 가운데에 배율 k 로 넣는다(편성 자리 · 편성 보유 목록 · 영웅 목록 미보유 = owned false).</summary>
public static Image CardIn ( Transform holder , OutGameSession s , HeroDb . Hero h , bool deployed , System . Action onTap , float k , bool owned = true )
{
return Fit ( Card ( holder , s , h , deployed , onTap , owned ) , k ) ;
}
/// <summary>미보유·도감 안내 한 줄(이름(한자) · 병종 · ★ · 보유/미보유).</summary>
public static string InfoText ( HeroDb . Hero h , bool own )
2026-11-09 04:56:07 +00:00
{
2026-11-10 01:49:27 +00:00
return h . nameKo + ( string . IsNullOrEmpty ( h . hanja ) ? "" : "(" + h . hanja + ")" ) + " · " + UnitTypeInfo . Korean ( h . type ) + " · ★" + h . rarity +
( h . isSoldier ? " · 병사 유닛" : "" ) + ( own ? " · 보유" : " · 미보유 — 거래 > 뽑기" ) ;
2026-11-09 04:56:07 +00:00
}
public static Image EmptyIn ( Transform holder , float k )
{
return Fit ( Empty ( holder ) , k ) ;
}
static Image Fit ( Image card , float k )
{
var rt = card . rectTransform ;
rt . anchorMin = rt . anchorMax = new Vector2 ( 0.5f , 0.5f ) ;
rt . pivot = new Vector2 ( 0.5f , 0.5f ) ;
rt . sizeDelta = CellSize ;
rt . anchoredPosition = Vector2 . zero ;
rt . localScale = new Vector3 ( k , k , 1f ) ;
return card ;
}
2026-11-09 03:44:06 +00:00
/// <summary>빈 카드(미보유 자리) = 어두운 바탕 + 문양 + 테두리 + 오른쪽 아래 얼굴 실루엣(팩 CardFrame01_Empty).</summary>
public static Image Empty ( Transform content )
{
var cell = UiKit . Cell ( content , "Empty" , Color . clear , null ) ;
cell . sprite = null ;
var t = cell . transform ;
2026-11-09 04:56:07 +00:00
// 팩 CardFrame01_Empty 그대로: Bg(#303146 · 마스크) 안에 Bg_d_Pattern 타일(#2D2F42) → 테두리 → 얼굴
var bg = UiKit . Layer ( t , "Bg" , "card_bg" , UiKit . Col ( "card_empty" , UiKit . Slot ) , 2f , 2f , 2f , 2f ) ;
bg . gameObject . AddComponent < Mask > ( ) . showMaskGraphic = true ;
var pat = UiKit . Layer ( bg . transform , "Pattern" , "card_empty_pattern" , UiKit . Col ( "card_empty_pattern" , UiKit . Slot ) ) ;
2026-11-09 03:44:06 +00:00
pat . type = Image . Type . Tiled ;
2026-11-09 04:56:07 +00:00
pat . pixelsPerUnitMultiplier = 1f ;
2026-11-09 03:44:06 +00:00
UiKit . Layer ( t , "Border" , "card_border" , UiKit . Col ( "card_empty_border" , UiKit . Line ) ) ;
var face = UiKit . Icon ( t , "face" , new Vector2 ( 1f , 0f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( - 87f , 90.8f ) , 127f ) ;
if ( face ! = null ) face . color = UiKit . Col ( "card_empty_face" , UiKit . Dark ) ;
return cell ;
}
}
/// <summary>
/// 영웅 탭 = 영웅 목록(PD 2026-11-09 레퍼런스 = 팩 Collection_List) — 헤더(제목 · 장수/병사 고르기 ▼ · 사선 칸 「보유 / 전체」) ·
2026-11-10 01:49:27 +00:00
/// 세로 카드(보유 = 레벨 높은 순 → 탭 = 영웅 상세) + 미보유 = 그 영웅 카드(조금 어둡게 · 「미보유」 · 탭 = 획득 안내 · PD 2026-11-10).
2026-11-09 03:44:06 +00:00
/// </summary>
2026-11-05 12:11:10 +00:00
public class HeroListScreen : OutGameScreen
{
bool soldiers ;
RectTransform content ;
ScrollRect scroll ;
2026-11-09 03:44:06 +00:00
Text filterText ;
Text countText ;
int ownedCards ;
2026-11-10 01:49:27 +00:00
int lockedCards ;
2026-11-09 10:30:58 +00:00
UiAutoGrid autoGrid ;
2026-11-05 12:11:10 +00:00
public bool ShowingSoldiers
{
get { return soldiers ; }
}
public override void Build ( )
{
Tab = 0 ;
2026-11-09 03:44:06 +00:00
// 바탕 = 진남(레퍼런스 실측) + 재화 바 뒤 띠
UiKit . Paint ( UiKit . Fill ( "ListBg" , Rt ) , UiKit . Col ( "list_bg" , UiKit . Bg ) , false ) ;
UiKit . Paint ( UiKit . TopBand ( "ListTop" , Rt , 0f , OutGameRoot . TopBarH ) , UiKit . Col ( "band_top" , UiKit . Bg ) , false ) ;
var head = Header ( "영웅" , "" , true ) ;
// 오른쪽 사선 칸(보유 / 전체) — 팩 PanelFrame03_Oblique #D8E8FF 239 폭
var ob = UiKit . Rect ( "Oblique" , head , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , Vector2 . zero , new Vector2 ( 239f , OutGameRoot . HeaderH - 7f ) ) ;
UiKit . Skin ( ob , "oblique" , UiKit . Col ( "oblique" , UiKit . White ) ) ;
countText = UiKit . Title ( UiKit . Label ( ob , "" , 52 , UiKit . Col ( "count_text" , UiKit . Dark ) , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 12f , 0f ) , new Vector2 ( 220f , 80f ) ) ) ;
countText . horizontalOverflow = HorizontalWrapMode . Overflow ;
// 장수 / 병사 고르기(팩 「Property ▼」 자리 · 탭 = 전환)
var pick = UiKit . Rect ( "Filter" , head , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 252f , 0f ) , new Vector2 ( 240f , 90f ) ) ;
var hit = pick . gameObject . AddComponent < Image > ( ) ;
hit . color = Color . clear ;
pick . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ( ) = > SetFilter ( ! soldiers ) ) ;
filterText = UiKit . Title ( UiKit . Label ( pick , "" , 38 , UiKit . InkSoft , TextAnchor . MiddleRight , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 52f , 2f ) , new Vector2 ( 180f , 70f ) ) ) ;
var arrow = UiKit . Icon ( pick , "arrow_down" , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 6f , 0f ) , 38f ) ;
UiKit . DropShadow ( arrow , 3f ) ;
2026-11-09 10:30:58 +00:00
// 본문 = 화면 폭 전체(해상도 대응 · PD 2026-11-09) · 열 수·카드 배율 = UiAutoGrid(기준 1080 = 4열 · 좁으면 3 · 넓으면 5 이상)
var body = UiKit . Fill ( "ListBody" , Rt , 0f , 0f , OutGameRoot . TopBarH + OutGameRoot . HeaderH , OutGameRoot . TabBarH ) ;
content = UiKit . ScrollGrid ( "List" , body , 24f , 24f , 20f , 0f , CollectionCards . CellSize , CollectionCards . Spacing , 4 , out scroll ) ;
autoGrid = content . gameObject . AddComponent < UiAutoGrid > ( ) ;
autoGrid . design = CollectionCards . CellSize ;
}
/// <summary>열 수(해상도 대응 · 테스트·캡처).</summary>
public int Columns
{
get { return autoGrid ! = null ? autoGrid . Columns : 0 ; }
}
/// <summary>격자 칸(holder) — 원래 크기 카드를 UiAutoGrid 배율로 넣는다.</summary>
Transform Slot ( )
{
var go = new GameObject ( "Slot" , typeof ( RectTransform ) ) ;
go . transform . SetParent ( content , false ) ;
return go . transform ;
2026-11-05 12:11:10 +00:00
}
public void SetFilter ( bool soldier )
{
soldiers = soldier ;
Refresh ( ) ;
}
public override void Refresh ( )
{
2026-11-09 03:44:06 +00:00
filterText . text = soldiers ? "병사" : "장수" ;
2026-11-05 12:11:10 +00:00
UiKit . Clear ( content ) ;
2026-11-09 03:44:06 +00:00
var owned = new List < HeroDb . Hero > ( S . Owned ( soldiers ) ) ;
// 보유 = 레벨 높은 순 → ★ 높은 순
owned . Sort ( ( a , b ) = >
{
int la = S . Save . Level ( a . id ) ! = null ? S . Save . Level ( a . id ) . lv : 1 , lb = S . Save . Level ( b . id ) ! = null ? S . Save . Level ( b . id ) . lv : 1 ;
if ( la ! = lb ) return lb . CompareTo ( la ) ;
var ha = S . Save . Hero ( a . id ) ;
var hb = S . Save . Hero ( b . id ) ;
return ( hb ! = null ? hb . star : b . rarity ) . CompareTo ( ha ! = null ? ha . star : a . rarity ) ;
} ) ;
ownedCards = 0 ;
foreach ( var h in owned )
2026-11-05 12:11:10 +00:00
{
var id = h . id ;
2026-11-09 10:30:58 +00:00
CollectionCards . CardIn ( Slot ( ) , S , h , S . InActivePreset ( id ) , ( ) = > Root . ShowHero ( id ) , autoGrid . Scale ) ;
2026-11-09 03:44:06 +00:00
ownedCards + + ;
2026-11-05 12:11:10 +00:00
}
2026-11-10 01:49:27 +00:00
// 미보유 = 빈 카드 대신 그 영웅 카드(일러 조금 어둡게 · 「미보유」 · 탭 = 획득 안내) · ★ 높은 순 → 표 순서
// PD 2026-11-10 「왜 유닛 목록에 장수들의 모습이 보이지 않는거지? 이번에 추가된 리소스가 반영되도록」
// (상세 화면은 레벨·세이브가 있는 보유 영웅 전제라 미보유는 열지 않는다)
var locked = new List < HeroDb . Hero > ( ) ;
var all = S . Db . Heroes . heroes ;
foreach ( var h in all )
if ( h . isSoldier = = soldiers & & ! S . Save . Owns ( h . id ) ) locked . Add ( h ) ;
locked . Sort ( ( a , b ) = > a . rarity ! = b . rarity ? b . rarity . CompareTo ( a . rarity ) : all . IndexOf ( a ) . CompareTo ( all . IndexOf ( b ) ) ) ;
lockedCards = 0 ;
foreach ( var h in locked )
{
var hero = h ;
CollectionCards . CardIn ( Slot ( ) , S , h , false , ( ) = > Root . Toast ( CollectionCards . InfoText ( hero , false ) ) , autoGrid . Scale , false ) ;
lockedCards + + ;
}
countText . text = owned . Count + "/" + ( owned . Count + locked . Count ) ;
2026-11-09 10:30:58 +00:00
autoGrid . Apply ( ) ;
2026-11-05 12:11:10 +00:00
}
2026-11-10 01:49:27 +00:00
/// <summary>보유 카드 수(미보유 제외).</summary>
2026-11-05 12:11:10 +00:00
public int CardCount
{
2026-11-09 03:44:06 +00:00
get { return ownedCards ; }
2026-11-05 12:11:10 +00:00
}
2026-11-10 01:49:27 +00:00
/// <summary>미보유 영웅 카드 수(그림 카드 · PD 2026-11-10).</summary>
public int LockedCount
{
get { return lockedCards ; }
}
2026-11-05 12:11:10 +00:00
}
/// <summary>
2026-11-09 02:11:20 +00:00
/// 영웅 상세(UI 상업용 재설계 ① · PD 2026-11-09 확정 목업 hero_detail.html = 좌표·부품·색 SOT · 본문 y = 목업 y − 224) —
/// 등급 색 바탕(글로우 + 문양 · 화면 전체) · 전투력 칩 · 일러(좌우 밀기 = 영웅 전환) + 양옆 장비 6칸 · 레벨 행(레벨업 · 승급) · 이름 띠(세력 · 병종 · 등급 · ★) ·
/// 스킬 패널(액티브 · 패시브 · 행마다 레벨업) · 메뉴 4버튼(정보 = 상세정보 + 도감 · 진화 = 조각 팝업 · 각성 · 외형 = 준비 중). 색·스프라이트 = 테마 표(ui_theme.csv) id.
2026-11-05 12:11:10 +00:00
/// </summary>
public class HeroDetailScreen : OutGameScreen
{
2026-11-09 02:11:20 +00:00
const float SwipeMin = 80f ; // 일러 밀기 = 가로 80 px 이상(기준 해상도 px)
static readonly string [ ] MenuNames = { "정보" , "진화" , "각성" , "외형" } ;
static readonly string [ ] MenuIcons = { "info" , "star_l" , "fire" , "top" } ;
static readonly Vector2 [ ] MenuIconSize = { new Vector2 ( 26f , 60f ) , new Vector2 ( 58f , 62f ) , new Vector2 ( 64f , 50f ) , new Vector2 ( 62f , 46f ) } ;
2026-11-05 12:11:10 +00:00
string id ;
readonly List < string > order = new List < string > ( ) ;
2026-11-09 02:11:20 +00:00
Image bgFill ;
Image bgGlow ;
Text ownedText ;
2026-11-05 12:11:10 +00:00
Text power ;
RectTransform art ;
2026-11-09 02:11:20 +00:00
float dragX ;
readonly GameObject [ ] slotRoot = new GameObject [ 6 ] ;
readonly Image [ ] slotGlow = new Image [ 6 ] ;
readonly Image [ ] slotBg = new Image [ 6 ] ;
readonly Image [ ] slotDeco = new Image [ 6 ] ;
readonly Image [ ] slotLight = new Image [ 6 ] ;
2026-11-06 14:20:57 +00:00
readonly Image [ ] slotIcon = new Image [ 6 ] ;
2026-11-09 02:11:20 +00:00
readonly Text [ ] slotName = new Text [ 6 ] ;
readonly Image [ ] slotCheck = new Image [ 6 ] ;
readonly Image [ ] slotBag = new Image [ 6 ] ;
Text lvBadge ;
2026-11-05 12:11:10 +00:00
Text lvText ;
Text expText ;
2026-11-09 02:11:20 +00:00
RectTransform expFill ;
Image levelImg ;
Text levelLabel ;
Text levelCost ;
Image goldIcon ;
Image promoteImg ;
Text promoteSub ;
Image bandImg ;
2026-11-09 10:59:56 +00:00
RectTransform factionRoot ;
2026-11-09 02:11:20 +00:00
Image clsIcon ;
Image paraImg ;
Text gradeText ;
Text nameText ;
Text typeText ;
RectTransform starRow ;
Image panelImg ;
GameObject coolChip ;
Text coolText ;
readonly Image [ ] rowImg = new Image [ 2 ] ;
readonly Image [ ] skFrame = new Image [ 2 ] ;
readonly Image [ ] skCls = new Image [ 2 ] ;
readonly Text [ ] skName = new Text [ 2 ] ;
readonly Text [ ] skHanja = new Text [ 2 ] ;
readonly Text [ ] skDesc = new Text [ 2 ] ;
readonly Text [ ] skLv = new Text [ 2 ] ;
readonly Image [ ] skBtn = new Image [ 2 ] ;
readonly Text [ ] skBtnText = new Text [ 2 ] ;
readonly Text [ ] skBtnSub = new Text [ 2 ] ;
readonly GameObject [ ] skCost = new GameObject [ 2 ] ;
readonly Text [ ] skBooks = new Text [ 2 ] ;
readonly Text [ ] skGold = new Text [ 2 ] ;
Image evolveDot ;
2026-11-05 12:11:10 +00:00
public string HeroId
{
get { return id ; }
}
public void Select ( string heroId )
{
id = heroId ;
var h = S . Def ( heroId ) ;
order . Clear ( ) ;
if ( h ! = null )
foreach ( var o in S . Owned ( h . isSoldier ) ) order . Add ( o . id ) ;
}
2026-11-09 02:11:20 +00:00
// ---------- 목업 좌표 도우미(부모 왼쪽 위 기준 · y 아래로) ----------
static Color Col ( string colorId )
{
return UISkin . Col ( colorId , Color . white ) ;
}
static RectTransform At ( Transform p , string name , float x , float y , float w , float h )
{
return UiKit . Rect ( name , p , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( x , - y ) , new Vector2 ( w , h ) ) ;
}
static Image Spr ( Transform p , string name , string skin , float x , float y , float w , float h , string color , bool raycast = false )
{
return UiKit . Skin ( At ( p , name , x , y , w , h ) , skin , Col ( color ) , raycast ) ;
}
Image Press ( Transform p , string name , string skin , float x , float y , float w , float h , string color , System . Action onClick )
{
var img = Spr ( p , name , skin , x , y , w , h , color , true ) ;
img . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ( ) = > onClick ( ) ) ;
return img ;
}
static Image Ico ( Transform p , string icon , float x , float y , float w , float h , float shadow = 0f )
{
var img = At ( p , "Icon" , x , y , w , h ) . gameObject . AddComponent < Image > ( ) ;
img . preserveAspect = true ;
img . raycastTarget = false ;
SetIcon ( img , icon , 1f ) ;
if ( shadow > 0f ) Drop ( img , shadow ) ;
return img ;
}
/// <summary>테마 아이콘(팩 임시 = 표 tint · 신규 제작 = 원색) · alpha = 곱.</summary>
static void SetIcon ( Image img , string icon , float alpha )
{
var e = UISkin . Theme ! = null & & icon ! = null ? UISkin . Theme . IconEntryOf ( icon ) : null ;
img . sprite = e ! = null ? e . Sprite : null ;
var c = e ! = null & & e . samguk = = null ? e . tint : Color . white ;
c . a * = alpha ;
img . color = c ;
img . enabled = img . sprite ! = null ;
}
static void Drop ( Graphic g , float d )
{
var s = g . gameObject . AddComponent < Shadow > ( ) ;
s . effectColor = Col ( "icon_shadow" ) ;
s . effectDistance = new Vector2 ( 0f , - d ) ;
}
/// <summary>목업 text() — y = 글자 윗변 · title = Sen(+ 한글 폴백) · false = 맑은 고딕 · stroke < 0 = 기본(크기 × 0.06) · 0 = 없음 · 그 밖 = 목업 px(절반).</summary>
static Text Txt ( Transform p , string s , float x , float y , int size , string color , TextAnchor align = TextAnchor . MiddleLeft , float w = 0f , bool title = true , float stroke = - 1f , bool shadow = true )
{
float bw = w > 0f ? w : 900f ;
float px = align = = TextAnchor . MiddleCenter ? 0.5f : align = = TextAnchor . MiddleRight ? 1f : 0f ;
var rt = UiKit . Rect ( "Text" , p , new Vector2 ( 0f , 1f ) , new Vector2 ( px , 0.5f ) , new Vector2 ( x + bw * px , - ( y + size * 0.5f ) ) , new Vector2 ( bw , size * 1.6f ) ) ;
var t = rt . gameObject . AddComponent < Text > ( ) ;
t . font = title ? UISkin . TitleFont : TextUtil . Font ;
t . fontSize = size ;
t . fontStyle = FontStyle . Bold ;
t . alignment = align ;
t . color = Col ( color ) ;
t . horizontalOverflow = HorizontalWrapMode . Overflow ;
t . verticalOverflow = VerticalWrapMode . Overflow ;
t . supportRichText = true ;
t . raycastTarget = false ;
t . text = s ;
if ( stroke ! = 0f ) UISkin . Stroke ( t , Col ( "stroke" ) , stroke < 0f ? Mathf . Max ( 2f , size * 0.06f ) : stroke * 0.5f ) ;
if ( shadow )
{
var sh = t . gameObject . AddComponent < Shadow > ( ) ;
sh . effectColor = Col ( "text_shadow" ) ;
sh . effectDistance = new Vector2 ( 0f , - Mathf . Round ( size * 0.07f ) ) ;
}
return t ;
}
static Text Desc ( Transform p , float x , float y , float w )
{
var t = Txt ( p , "" , x , y , 26 , "text_desc" , TextAnchor . UpperLeft , w , false , 0f , false ) ;
var rt = t . rectTransform ;
rt . pivot = new Vector2 ( 0f , 1f ) ;
rt . anchoredPosition = new Vector2 ( x , - y ) ;
rt . sizeDelta = new Vector2 ( w , 70f ) ;
t . horizontalOverflow = HorizontalWrapMode . Wrap ;
t . lineSpacing = 1.05f ;
return t ;
}
static void PlaceX ( Graphic g , float x )
{
var rt = g . rectTransform ;
rt . anchoredPosition = new Vector2 ( x , rt . anchoredPosition . y ) ;
}
// ---------- 화면 ----------
2026-11-05 12:11:10 +00:00
public override void Build ( )
{
Tab = 0 ;
2026-11-09 02:11:20 +00:00
// ① 바탕(화면 전체 · 재화 바·헤더 뒤까지) = 등급 색 + 세로 글로우 + 문양 타일
bgFill = UiKit . Fill ( "GradeBg" , Rt ) . gameObject . AddComponent < Image > ( ) ;
bgFill . raycastTarget = false ;
bgGlow = UiKit . Skin ( UiKit . Fill ( "GradeGlow" , Rt ) , "bg_glow" , Color . white ) ;
bgGlow . enabled = UISkin . Has ( "bg_glow" ) ;
var pat = UiKit . Skin ( UiKit . Fill ( "GradePattern" , Rt ) , "bg_pattern" , Col ( "grade_pattern" ) ) ;
pat . type = Image . Type . Tiled ;
pat . enabled = UISkin . Has ( "bg_pattern" ) ;
var head = Header ( "영웅" , "" , true ) ;
var own = UiKit . Rect ( "Owned" , head , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 24f , 0f ) , new Vector2 ( 244f , 62f ) ) ;
UiKit . Skin ( own , "chip" , Col ( "chip_dark" ) ) ;
ownedText = Txt ( own , "" , 0f , 15f , 32 , "white" , TextAnchor . MiddleCenter , 244f ) ;
// 본문 = 목업 1080 폭 틀(화면이 낮아 본문이 줄면서 가로로 늘어나도 가운데 고정)
var body = UiKit . Rect ( "Mock" , Body ( ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , Vector2 . zero , new Vector2 ( OutGameRoot . Reference . x , OutGameRoot . BodyH ) ) ;
// ② 일러 = 빛 원 + 발밑 그림자 + 그림(좌우 밀기 = 이전/다음 영웅)
Spr ( body , "ArtGlow" , "glow_circle" , 210f , 14f , 660f , 660f , "hero_glow" ) ;
Spr ( body , "ArtShadow" , "oval_shadow" , 310f , 550f , 460f , 64f , "hero_shadow" ) ;
art = At ( body , "Art" , 260f , 94f , 560f , 500f ) ;
art . gameObject . AddComponent < Image > ( ) . color = Color . clear ;
var drag = art . gameObject . AddComponent < TapDrag > ( ) ;
drag . OnDragStart = e = > dragX = 0f ;
drag . OnDragMove = e = > dragX + = e . delta . x ;
drag . OnDragEnd = e = > Swipe ( dragX / ScaleOf ( art ) ) ;
// 전투력 칩(두 장비 열 사이 위)
var pw = Spr ( body , "Power" , "chip" , 380f , 24f , 320f , 70f , "chip_dark" ) . transform ;
Spr ( pw , "Line" , "chip_line" , 6f , 6f , 308f , 58f , "chip_line" ) ;
Ico ( pw , "sword" , 18f , 4f , 62f , 62f ) ;
Txt ( pw , "전투력" , 88f , 23f , 28 , "text_soft" , TextAnchor . MiddleLeft , 0f , true , 0f , false ) ;
power = Txt ( pw , "" , 180f , 11f , 50 , "white" ) ;
// ◀ ▶ = 장비 칸 바깥(장비 열 세로 가운데)
Arrow ( body , "Prev" , "arrow_prev" , 10f , - 1 ) ;
Arrow ( body , "Next" , "arrow_next" , 1026f , 1 ) ;
// 장비 6칸(182× 178 · ItemFrame 5층 · 장비 등급 색) — 탭 = 장착 · 필요 장비
2026-11-05 12:11:10 +00:00
for ( int i = 0 ; i < 6 ; i + + )
{
int k = i ;
2026-11-09 02:11:20 +00:00
var s = At ( body , "Slot" + i , i < 3 ? 66f : 832f , 24f + ( i % 3 ) * 206f , 182f , 178f ) ;
slotRoot [ i ] = s . gameObject ;
slotGlow [ i ] = Spr ( s , "Glow" , "item_glow" , - 8f , - 8f , 198f , 194f , "white" ) ;
slotBg [ i ] = Spr ( s , "Bg" , "item_bg" , 0f , 0f , 182f , 178f , "white" , true ) ;
slotDeco [ i ] = Spr ( s , "Deco" , "item_deco" , 4f , 4f , 174f , 170f , "white" ) ;
slotLight [ i ] = Spr ( s , "Light" , "item_light" , 8f , 6f , 166f , 82f , "white" ) ;
Spr ( s , "Border" , "item_border" , 0f , 0f , 182f , 178f , "gear_border" ) ;
slotIcon [ i ] = Ico ( s , null , 41f , 17f , 100f , 100f , 5f ) ;
Spr ( s , "Plate" , "plate12" , 10f , 130f , 162f , 38f , "gear_plate" ) ;
slotName [ i ] = Txt ( s , "" , 10f , 135f , 26 , "white" , TextAnchor . MiddleCenter , 162f , false , 0f , false ) ;
slotCheck [ i ] = Ico ( s , "check" , 138f , - 12f , 54f , 52f , 3f ) ;
slotBag [ i ] = Ico ( s , "up_green" , 142f , - 14f , 48f , 56f , 3f ) ;
s . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ( ) = > OnSlot ( k ) ) ;
}
// ③ 레벨 행 = 배지 + 레벨 바 · 레벨업(녹 Primary + 골드) · 승급(하늘)
Ico ( body , "lv_badge" , 30f , 650f , 92f , 96f , 3f ) ;
lvBadge = Txt ( body , "" , 30f , 674f , 46 , "white" , TextAnchor . MiddleCenter , 92f ) ;
lvText = Txt ( body , "" , 132f , 648f , 34 , "white" ) ;
expText = Txt ( body , "" , 400f , 654f , 28 , "text_desc" , TextAnchor . MiddleRight , 190f , false , 3f ) ;
Spr ( body , "LvBar" , "lv_bg" , 116f , 694f , 478f , 54f , "white" ) ;
expFill = ( RectTransform ) Spr ( body , "LvFill" , "lv_fill" , 126f , 700f , 458f , 38f , "lv_fill" ) . transform ;
levelImg = Press ( body , "LevelUp" , "btn_l_green" , 612f , 640f , 208f , 112f , "white" , OnLevelUp ) ;
levelLabel = Txt ( levelImg . transform , "레벨업" , 0f , 16f , 38 , "text_on_green" , TextAnchor . MiddleCenter , 208f , false , 0f , false ) ;
goldIcon = Ico ( levelImg . transform , "cur_gold" , 48f , 56f , 40f , 40f ) ;
levelCost = Txt ( levelImg . transform , "" , 92f , 60f , 32 , "text_on_green" , TextAnchor . MiddleLeft , 0f , true , 0f , false ) ;
promoteImg = Press ( body , "Promote" , "btn_sky" , 834f , 640f , 222f , 112f , "white" , OnPromote ) ;
Txt ( promoteImg . transform , "승급" , 0f , 14f , 38 , "white" , TextAnchor . MiddleCenter , 222f , false ) ;
promoteSub = Txt ( promoteImg . transform , "" , 0f , 60f , 28 , "white" , TextAnchor . MiddleCenter , 222f , false , 3f ) ;
// ④ 이름 띠 = 어두운 띠 + 세력 깃발 · 병종 원 · 등급 칩 · 이름 · 병종 · ★
bandImg = Spr ( body , "NameBand" , "card" , 24f , 776f , 1032f , 114f , "white" ) ;
Spr ( body , "BandLight" , "band_light" , 40f , 782f , 1000f , 12f , "band_light" ) ;
2026-11-09 10:59:56 +00:00
factionRoot = UiKit . At ( body , "Faction" , 44f , 789f , 88f , 88f ) ; // 진영 원 배지 자리(PD 2026-11-09 · 구 세력 깃발)
2026-11-09 11:20:15 +00:00
clsIcon = Ico ( body , null , 140f , 794f , 78f , 78f ) ; // 병과 = 평면 심볼만(배경 틀 · 그림자 없음 · PD 2026-11-09)
2026-11-09 02:11:20 +00:00
paraImg = Spr ( body , "Grade" , "para_green" , 232f , 805f , 96f , 56f , "white" ) ;
gradeText = Txt ( body , "" , 232f , 811f , 38 , "white" , TextAnchor . MiddleCenter , 96f ) ;
nameText = Txt ( body , "" , 344f , 797f , 66 , "white" ) ;
typeText = Txt ( body , "" , 500f , 819f , 30 , "text_type" , TextAnchor . MiddleLeft , 0f , false , 0f , false ) ;
starRow = At ( body , "Stars" , 742f , 803f , 290f , 60f ) ;
// ⑤ 스킬 패널 = 제목(액티브 + 쿨 칩 · 맨 오른쪽 Lv 칩 / 패시브 + Lv 칩) · 행 = 틀 + 병종 글리프 · 이름 · 한자 · 설명 · 레벨업
panelImg = Spr ( body , "SkillPanel" , "panel12" , 24f , 920f , 1032f , 444f , "white" ) ;
for ( int i = 0 ; i < 2 ; i + + )
{
int k = i ;
float ty = 934f + i * 214f , y = ty + 54f ;
Txt ( body , i = = 0 ? "액티브" : "패시브" , 52f , ty + 4f , 36 , "white" ) ;
if ( i = = 0 )
2026-11-06 14:20:57 +00:00
{
2026-11-09 02:11:20 +00:00
var cc = Spr ( body , "Cool" , "chip" , 184f , ty , 112f , 46f , "chip_cool" ) ;
coolChip = cc . gameObject ;
coolText = Txt ( cc . transform , "" , 0f , 9f , 26 , "white" , TextAnchor . MiddleCenter , 112f , false , 3f ) ;
2026-11-06 14:20:57 +00:00
}
2026-11-09 02:11:20 +00:00
var lc = Spr ( body , "SkillLv" + i , "chip" , 912f , ty , 112f , 46f , "chip_dark" ) ;
skLv [ i ] = Txt ( lc . transform , "" , 0f , 9f , 26 , "white" , TextAnchor . MiddleCenter , 112f , true , 3f ) ;
rowImg [ i ] = Spr ( body , "SkillRow" + i , "panel12" , 44f , y , 992f , 146f , "white" ) ;
skFrame [ i ] = Ico ( body , null , 58f , y + 10f , 126f , 126f , 4f ) ;
2026-11-09 11:20:15 +00:00
skCls [ i ] = Ico ( body , null , 80f , y + 32f , 82f , 82f ) ;
2026-11-09 02:11:20 +00:00
skName [ i ] = Txt ( body , "" , 200f , y + 18f , 42 , "white" ) ;
skHanja [ i ] = Txt ( body , "" , 200f , y + 30f , 28 , "white" , TextAnchor . MiddleLeft , 0f , false , 0f , false ) ;
skDesc [ i ] = Desc ( body , 200f , y + 74f , 600f ) ;
skBtn [ i ] = Press ( body , "SkillUp" + i , "btn_purple" , 818f , y + 18f , 204f , 110f , "white" , ( ) = > OnSkillRow ( k ) ) ;
var bt = skBtn [ i ] . transform ;
skBtnText [ i ] = Txt ( bt , "" , 0f , 12f , 34 , "white" , TextAnchor . MiddleCenter , 204f , false ) ;
skBtnSub [ i ] = Txt ( bt , "" , 0f , 58f , 24 , "text_max_sub" , TextAnchor . MiddleCenter , 204f , false , 3f ) ;
var cost = At ( bt , "Cost" , 0f , 0f , 204f , 110f ) ;
skCost [ i ] = cost . gameObject ;
Ico ( cost , "cur_skillbook" , 22f , 56f , 38f , 38f ) ;
skBooks [ i ] = Txt ( cost , "" , 62f , 60f , 28 , "white" , TextAnchor . MiddleLeft , 0f , true , 3f ) ;
Ico ( cost , "cur_gold" , 98f , 59f , 32f , 32f ) ;
skGold [ i ] = Txt ( cost , "" , 132f , 63f , 22 , "text_book" , TextAnchor . MiddleLeft , 0f , true , 3f ) ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 02:11:20 +00:00
// ⑥ 메뉴 4버튼 = 정보 · 진화 · 각성 · 외형(진화 가능 = 알림 점)
for ( int i = 0 ; i < 4 ; i + + )
2026-11-05 12:11:10 +00:00
{
2026-11-09 02:11:20 +00:00
int k = i ;
var b = Press ( body , "Menu" + i , "round08" , 34f + i * 258f , 1390f , 236f , 108f , "menu_btn" , ( ) = > OnMenu ( k ) ) ;
Spr ( b . transform , "Light" , "band_light" , 14f , 8f , 208f , 12f , "menu_light" ) ;
var sz = MenuIconSize [ i ] ;
Ico ( b . transform , MenuIcons [ i ] , 30f + ( 64f - sz . x ) / 2f , 54f - sz . y / 2f , sz . x , sz . y , 3f ) ;
Txt ( b . transform , MenuNames [ i ] , 104f , 34f , 40 , "white" , TextAnchor . MiddleLeft , 0f , false ) ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 02:11:20 +00:00
evolveDot = Ico ( body , "new_dot" , 498f , 1380f , 36f , 36f ) ;
}
2026-11-05 12:11:10 +00:00
2026-11-09 02:11:20 +00:00
void Arrow ( Transform body , string name , string icon , float x , int d )
{
var hit = At ( body , name , x - 18f , 249f , 80f , 140f ) ;
hit . gameObject . AddComponent < Image > ( ) . color = Color . clear ;
hit . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ( ) = > Step ( d ) ) ;
Ico ( hit , icon , 18f , 27f , 44f , 86f ) ;
}
static float ScaleOf ( Transform t )
{
var cv = t . GetComponentInParent < Canvas > ( ) ;
return cv ! = null & & cv . scaleFactor > 0f ? cv . scaleFactor : 1f ;
}
/// <summary>일러 밀기(기준 px · 왼쪽 = 다음 · 오른쪽 = 이전 · 80 px 미만 = 무시).</summary>
public void Swipe ( float dx )
{
if ( Mathf . Abs ( dx ) > = SwipeMin ) Step ( dx < 0f ? 1 : - 1 ) ;
2026-11-05 12:11:10 +00:00
}
void Step ( int d )
{
if ( order . Count = = 0 ) return ;
int i = Mathf . Max ( 0 , order . IndexOf ( id ) ) ;
id = order [ ( i + d + order . Count ) % order . Count ] ;
Refresh ( ) ;
}
public override void Refresh ( )
{
var h = S . Def ( id ) ;
if ( h = = null ) return ;
var db = S . Db ;
var lv = S . Save . Level ( id ) ;
var hs = S . Save . Hero ( id ) ;
var st = S . StatsOf ( id ) ;
2026-11-09 02:11:20 +00:00
string g = h . isSoldier | | hs = = null | | string . IsNullOrEmpty ( hs . grade ) ? "white" : hs . grade ;
string G = "grade_" + g + "_" ;
bgFill . color = Col ( G + "bg" ) ;
bgGlow . color = Col ( G + "glow" ) ;
bandImg . color = Col ( G + "band" ) ;
panelImg . color = Col ( G + "stat" ) ;
for ( int i = 0 ; i < 2 ; i + + ) rowImg [ i ] . color = Col ( G + "row" ) ;
if ( UISkin . Apply ( paraImg , "para_" + g ) ) paraImg . color = Col ( G + "chip" ) ;
paraImg . gameObject . SetActive ( ! h . isSoldier ) ;
gradeText . gameObject . SetActive ( ! h . isSoldier ) ;
gradeText . text = UiKit . GradeKo ( g ) ;
int total = 0 ;
foreach ( var o in db . Heroes . heroes )
if ( o . isSoldier = = h . isSoldier ) total + + ;
ownedText . text = "보유 " + S . Owned ( h . isSoldier ) . Count + " / " + total ;
power . text = st . power . ToString ( "N0" ) ;
2026-11-05 12:11:10 +00:00
UiKit . Clear ( art ) ;
2026-11-09 02:11:20 +00:00
UiKit . Portrait ( "Hero" , art , h , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , new Vector2 ( 560f , 500f ) , false , 1f , Color . clear ) ;
2026-11-05 12:11:10 +00:00
2026-11-09 02:11:20 +00:00
// 장비: 장착 = 체크 · 가방 보유 = 초록 화살표 · 없음 = 흐림
2026-11-05 12:11:10 +00:00
for ( int i = 0 ; i < 6 ; i + + )
{
string need = h . isSoldier ? null : HeroRules . RequiredGear ( db , h , hs , i ) ;
2026-11-09 02:11:20 +00:00
slotRoot [ i ] . SetActive ( need ! = null ) ;
2026-11-05 12:11:10 +00:00
if ( need = = null ) continue ;
2026-11-09 02:11:20 +00:00
var gd = db . Gear . Get ( need ) ;
bool on = hs ! = null & & i < hs . gear . Count & & ! string . IsNullOrEmpty ( hs . gear [ i ] ) ;
bool bag = ! on & & S . Save . inv . GearCount ( need ) > 0 ;
bool lit = on | | bag ;
string gg = "gear_" + ( gd ! = null & & ! string . IsNullOrEmpty ( gd . grade ) ? gd . grade : "white" ) + "_" ;
slotGlow [ i ] . color = lit ? Col ( gg + "glow" ) : Color . clear ;
slotBg [ i ] . color = Col ( gg + "bg" ) ;
slotDeco [ i ] . color = Col ( gg + "deco" ) ;
slotLight [ i ] . color = Col ( gg + "light" ) ;
SetIcon ( slotIcon [ i ] , gd ! = null ? "gear_" + gd . kind : null , lit ? 1f : Col ( "gear_off_icon" ) . a ) ;
slotIcon [ i ] . GetComponent < Shadow > ( ) . enabled = lit ;
slotName [ i ] . text = gd ! = null ? gd . nameKo : need ;
slotName [ i ] . color = Col ( lit ? "white" : "text_off" ) ;
slotCheck [ i ] . gameObject . SetActive ( on & & slotCheck [ i ] . sprite ! = null ) ;
slotBag [ i ] . gameObject . SetActive ( bag & & slotBag [ i ] . sprite ! = null ) ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 02:11:20 +00:00
// 레벨 · 승급 — 구조적으로 불가(최고 · 병사) = 회색 · 조건 부족 = 원래 색 + 누르면 사유 토스트
int lvCap = HeroRules . LevelCap ( db , S . Save ) ;
var row = db . Levels . Get ( lv . lv ) ;
bool maxLv = lv . lv > = db . Levels . MaxLevel | | row = = null ;
float pct = row ! = null & & row . expToNext > 0 ? Mathf . Clamp01 ( lv . exp / ( float ) row . expToNext ) : 1f ;
lvBadge . text = lv . lv . ToString ( ) ;
lvText . text = "Lv " + lv . lv + " / " + lvCap ;
expText . text = maxLv ? "최고 레벨" : "경험치 " + ( pct * 100f ) . ToString ( "0.##" ) + "%" ;
expFill . sizeDelta = new Vector2 ( 458f * pct , expFill . sizeDelta . y ) ;
expFill . gameObject . SetActive ( pct > 0.01f ) ;
levelImg . color = Col ( maxLv ? "disabled" : "white" ) ;
levelLabel . color = levelCost . color = Col ( maxLv ? "disabled_text" : "text_on_green" ) ;
levelCost . text = maxLv ? "최고" : row . goldCost . ToString ( "N0" ) ;
goldIcon . gameObject . SetActive ( ! maxLv ) ;
var gearList = h . isSoldier | | hs = = null ? null : db . Heroes . GearFor ( h , hs . grade ) ;
promoteSub . text = h . isSoldier ? "병사 없음" : gearList = = null ? "최고 등급" : "장비 " + HeroRules . EquippedCount ( hs ) + "/" + gearList . Count ;
promoteImg . color = Col ( gearList = = null ? "disabled" : "white" ) ;
evolveDot . gameObject . SetActive ( ! h . isSoldier & & hs ! = null & & evolveDot . sprite ! = null & & HeroRules . CanEvolve ( db , h , hs ) = = null ) ;
// 이름 띠
2026-11-09 10:59:56 +00:00
UiKit . Clear ( factionRoot ) ;
UiKit . DropShadow ( UiKit . FactionBadge ( factionRoot , h . faction , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , 84f ) , 3f ) ;
SetIcon ( clsIcon , UiKit . TypeSymbolId ( h . type ) , 1f ) ;
2026-11-05 12:11:10 +00:00
nameText . text = h . nameKo ;
2026-11-09 02:11:20 +00:00
typeText . text = UnitTypeInfo . Korean ( h . type ) ;
PlaceX ( typeText , 344f + nameText . preferredWidth + 24f ) ;
UiKit . Clear ( starRow ) ;
int max = h . isSoldier ? 3 : db . Evolve . MaxStar ;
int n = h . isSoldier ? h . rarity : hs ! = null ? hs . star : 1 ;
for ( int i = 0 ; i < max ; i + + ) Ico ( starRow , i < n ? "star_l" : "star_off" , i * 58f , 0f , 58f , 60f , i < n ? 3f : 0f ) ;
// 스킬 — 상한(등급·★3) 또는 최고 = 회색 「최대」 · 그 밖 = 보라 레벨업(스킬서 · 골드)
string tk = UiKit . TypeKey ( h . type ) ;
2026-11-05 12:11:10 +00:00
for ( int i = 0 ; i < 2 ; i + + )
{
2026-11-09 02:11:20 +00:00
var sk = SkillRules . Skill ( db , h , i ) ;
bool has = sk ! = null & & ! h . isSoldier & & hs ! = null ;
SetIcon ( skFrame [ i ] , tk ! = null ? "skill_" + tk + ( i = = 0 ? "_a" : "_p" ) : null , has ? 1f : 0.5f ) ;
2026-11-09 10:59:56 +00:00
SetIcon ( skCls [ i ] , UiKit . TypeSymbolId ( h . type ) , has ? 1f : 0.5f ) ;
2026-11-09 02:11:20 +00:00
skLv [ i ] . transform . parent . gameObject . SetActive ( has ) ;
skBtn [ i ] . gameObject . SetActive ( has ) ;
if ( i = = 0 ) coolChip . SetActive ( has ) ;
if ( ! has )
2026-11-05 12:11:10 +00:00
{
2026-11-09 02:11:20 +00:00
skName [ i ] . text = h . isSoldier ? "스킬 없음" : "—" ;
skHanja [ i ] . text = "" ;
skDesc [ i ] . text = h . isSoldier ? "병사 유닛은 스킬이 없습니다" : "" ;
2026-11-05 12:11:10 +00:00
continue ;
}
2026-11-09 02:11:20 +00:00
int slv = SkillRules . Level ( hs , i ) , cap = SkillRules . Cap ( db , h , hs , i ) ;
skName [ i ] . text = sk . nameKo ;
skHanja [ i ] . text = sk . hanja ? ? "" ;
skHanja [ i ] . color = Col ( G + "label" ) ;
PlaceX ( skHanja [ i ] , 200f + skName [ i ] . preferredWidth + 8f ) ;
skDesc [ i ] . text = SkillInfoDb . DisplayDesc ( sk ) ;
skLv [ i ] . text = "Lv " + slv + "/" + cap ;
if ( i = = 0 ) coolText . text = "쿨 " + sk . cooldown + "턴" ;
bool top = slv > = cap ;
UISkin . Apply ( skBtn [ i ] , top ? "btn_gray" : "btn_purple" ) ;
skBtn [ i ] . color = Color . white ;
skBtnText [ i ] . text = top ? "최대" : "레벨업" ;
skBtnText [ i ] . color = Col ( top ? "text_max" : "white" ) ;
skBtnSub [ i ] . gameObject . SetActive ( top ) ;
skBtnSub [ i ] . text = slv > = sk . lvMax ? "최고 레벨" : "등급 상한" ;
skCost [ i ] . SetActive ( ! top ) ;
var cost = db . Skills . CostTo ( slv + 1 ) ;
skBooks [ i ] . text = cost ! = null ? cost . books . ToString ( ) : "-" ;
skGold [ i ] . text = cost ! = null ? cost . gold . ToString ( "N0" ) : "-" ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 02:11:20 +00:00
}
2026-11-05 12:11:10 +00:00
2026-11-09 02:11:20 +00:00
/// <summary>메뉴 4버튼: 0 정보(상세정보 + 도감) · 1 진화(조각 팝업) · 2 각성 · 3 외형(준비 중).</summary>
public void OnMenu ( int i )
{
switch ( i )
{
case 0 : ShowStats ( ) ; break ;
case 1 : ShowEvolve ( ) ; break ;
case 2 : Root . ComingSoon ( "각성" ) ; break ;
case 3 : Root . ComingSoon ( "외형" ) ; break ;
}
}
/// <summary>스킬 행 레벨업 = 그 스킬만(스킬서 + 골드 · 불가 = 사유 토스트).</summary>
public void OnSkillRow ( int i )
{
var h = S . Def ( id ) ;
var hs = S . Save . Hero ( id ) ;
if ( h = = null | | hs = = null | | h . isSoldier )
2026-11-05 12:11:10 +00:00
{
2026-11-09 02:11:20 +00:00
Root . Toast ( "병사 유닛은 스킬이 없습니다" ) ;
return ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 02:11:20 +00:00
var sk = SkillRules . Skill ( S . Db , h , i ) ;
if ( sk = = null ) return ;
var r = SkillRules . LevelUp ( S . Db , S . Save , h , hs , i ) ;
if ( r = = null ) S . Commit ( ) ;
Refresh ( ) ;
Root . Toast ( r ? ? sk . nameKo + " Lv " + SkillRules . Level ( hs , i ) + "!" ) ;
}
/// <summary>진화 팝업 = ★ 줄 + 영웅 조각 바 + 다음 ★ · 진화 버튼(기존 진화 규칙).</summary>
public void ShowEvolve ( )
{
var h = S . Def ( id ) ;
var hs = S . Save . Hero ( id ) ;
if ( h = = null | | hs = = null | | h . isSoldier )
2026-11-05 12:11:10 +00:00
{
2026-11-09 02:11:20 +00:00
Root . Toast ( "병사 유닛 — 진화 없음 (중복 = 골드)" ) ;
return ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 02:11:20 +00:00
var db = S . Db ;
int need = HeroRules . ShardsForNext ( db , hs ) ;
bool top = hs . star > = db . Evolve . MaxStar ;
var why = HeroRules . CanEvolve ( db , h , hs ) ;
var c = Root . ModalCustom ( "진화 — " + h . nameKo , 600f ,
new OutGameRoot . ModalButton ( top ? "최고 ★" : "진화" , why = = null ? UiKit . Red : UiKit . Disabled , OnEvolve , why ! = null ) ,
new OutGameRoot . ModalButton ( "닫기" , UiKit . Dark , null ) ) ;
UiKit . StarRow ( c , hs . star , db . Evolve . MaxStar , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 4f ) , 64f ) ;
UiKit . IconOrDot ( c , "cur_shard" , "조" , UiKit . Purple , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 20f , - 90f ) , 60f ) ;
UiKit . Label ( c , "영웅 조각" , 34 , UiKit . Ink , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 92f , - 90f ) , new Vector2 ( 400f , 60f ) ) ;
var bar = UiKit . Bar ( "ShardBar" , c , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 164f ) , new Vector2 ( 820f , 46f ) , UiKit . Slot , UiKit . Purple ) ;
UiKit . SetBar ( bar , need > 0 ? Mathf . Clamp01 ( hs . shards / ( float ) need ) : 1f ) ;
UiKit . Title ( UiKit . Label ( bar . parent , need > 0 ? hs . shards + " / " + need : hs . shards + " · 최고 ★" , 30 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , new Vector2 ( 800f , 46f ) ) , true ) ;
UiKit . Label ( c , top ? "최고 ★ 달성" : "다음 ★" + ( hs . star + 1 ) + " · 필요 조각 " + need + ( why ! = null ? "\n" + why : "" ) , 28 , UiKit . InkSoft , TextAnchor . UpperCenter ,
new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 226f ) , new Vector2 ( 840f , 84f ) , false ) ;
2026-11-05 12:11:10 +00:00
}
void OnSlot ( int i )
{
var h = S . Def ( id ) ;
var hs = S . Save . Hero ( id ) ;
if ( h = = null | | hs = = null ) return ;
var need = HeroRules . RequiredGear ( S . Db , h , hs , i ) ;
var g = S . Db . Gear . Get ( need ) ;
if ( need = = null ) return ;
if ( ! string . IsNullOrEmpty ( hs . gear [ i ] ) )
{
Root . Toast ( "장착됨: " + ( g ! = null ? g . nameKo : need ) + " — 6칸을 모두 채우면 승급" ) ;
return ;
}
var why = S . Equip ( id , i ) ;
if ( why = = null )
{
Root . Toast ( ( g ! = null ? g . nameKo : need ) + " 장착 · 1칸 = 1강(누적 " + hs . gearTotal + ")" ) ;
return ;
}
string from = g ! = null ? g . dropFromChapter + "장~ 스테이지 드랍" : "" ;
Root . Modal ( "필요 장비" , ( g ! = null ? g . nameKo + " (" + UiKit . GradeKo ( g . grade ) + " 등급)" : need ) + "\n보유 0\n\n획득: " + from + " · 거래 > 상점(흰/녹)" +
( g ! = null & & g . craftCount > 0 ? "\n합성 = 하위 " + g . craftCount + "개 + 도면 " + g . craftBlueprint + " (2차)" : "" ) ,
new OutGameRoot . ModalButton ( "상점 가기" , UiKit . Red , ( ) = > Root . Trade . OpenShop ( ) ) ,
new OutGameRoot . ModalButton ( "닫기" , UiKit . Dark , null ) ) ;
}
void OnLevelUp ( )
{
var why = S . LevelUp ( id ) ;
Root . Toast ( why ? ? S . Def ( id ) . nameKo + " Lv " + S . Save . Level ( id ) . lv + "!" ) ;
}
void OnPromote ( )
{
var h = S . Def ( id ) ;
var why = S . Promote ( id ) ;
Root . Toast ( why ? ? h . nameKo + " 승급 → " + UiKit . GradeKo ( S . Save . Hero ( id ) . grade ) + " 등급 · 새 장비 6칸" ) ;
}
2026-11-05 14:31:27 +00:00
/// <summary>스킬 레벨업(PD #1093 B): 액티브·패시브 중 고르기 → 스킬서 + 골드(skill_levels.csv) · 상한 = 등급 + ★3 액티브 +1.</summary>
public void OnSkillUp ( )
{
var h = S . Def ( id ) ;
var hs = S . Save . Hero ( id ) ;
if ( h = = null | | hs = = null | | h . isSoldier )
{
Root . Toast ( "병사 유닛은 스킬이 없습니다" ) ;
return ;
}
var db = S . Db ;
var buttons = new List < OutGameRoot . ModalButton > ( ) ;
var body = new System . Text . StringBuilder ( "보유 스킬서 " + S . Save . inv . books . skill + " · 골드 " + S . Save . gold . ToString ( "N0" ) + "\n" ) ;
for ( int i = 0 ; i < 2 ; i + + )
{
int k = i ;
var sk = SkillRules . Skill ( db , h , i ) ;
if ( sk = = null ) continue ;
int lv = SkillRules . Level ( hs , i ) ;
var cost = db . Skills . CostTo ( lv + 1 ) ;
var why = SkillRules . CanLevelUp ( db , S . Save , h , hs , i ) ;
body . Append ( "\n" ) . Append ( i = = 0 ? "액티브 " : "패시브 " ) . Append ( sk . nameKo ) . Append ( " Lv " ) . Append ( lv ) . Append ( " / " ) . Append ( SkillRules . Cap ( db , h , hs , i ) ) ;
if ( cost ! = null & & lv < sk . lvMax ) body . Append ( " → 스킬서 " ) . Append ( cost . books ) . Append ( " · 골드 " ) . Append ( cost . gold . ToString ( "N0" ) ) ;
if ( why ! = null ) body . Append ( "\n <color=#a03a22>" ) . Append ( why ) . Append ( "</color>" ) ;
buttons . Add ( new OutGameRoot . ModalButton ( ( i = = 0 ? "액티브" : "패시브" ) + " Lv+1" , why = = null ? UiKit . Red : UiKit . Disabled , ( ) = >
{
var r = SkillRules . LevelUp ( S . Db , S . Save , h , hs , k ) ;
if ( r = = null ) S . Commit ( ) ;
Refresh ( ) ;
if ( r = = null ) OnSkillUp ( ) ; // 이어서 올릴 수 있게 다시 연다
Root . Toast ( r ? ? sk . nameKo + " Lv " + SkillRules . Level ( hs , k ) + "!" ) ;
} ) ) ;
}
buttons . Add ( new OutGameRoot . ModalButton ( "닫기" , UiKit . Dark , null ) ) ;
Root . Modal ( "스킬 레벨업 — " + h . nameKo , body . ToString ( ) , buttons . ToArray ( ) ) ;
}
2026-11-05 12:11:10 +00:00
void OnEvolve ( )
{
var why = S . Evolve ( id ) ;
Root . Toast ( why ? ? S . Def ( id ) . nameKo + " 진화 → ★" + S . Save . Hero ( id ) . star ) ;
}
2026-11-09 02:11:20 +00:00
/// <summary>정보 = 상세정보(스탯 내역) + 도감 버튼.</summary>
public void ShowStats ( )
2026-11-05 12:11:10 +00:00
{
var h = S . Def ( id ) ;
var st = S . StatsOf ( id ) ;
var e = S . Db . Economy ;
var hs = S . Save . Hero ( id ) ;
string body =
"기본(" + UnitTypeInfo . Korean ( h . type ) + ") HP " + st . baseHp + " · 공 " + st . baseAtk + " · 방 " + st . baseDef + "\n" +
"영웅 배율 × " + st . hpMult + " · × " + st . atkMult + " · × " + st . defMult + "\n" +
"레벨 Lv " + S . Save . Level ( id ) . lv + " +" + st . lvPct . ToString ( "0.#" ) + "%\n" +
"승급 " + ( hs ! = null ? UiKit . GradeKo ( hs . grade ) : "—" ) + " +" + st . promoPct . ToString ( "0.#" ) + "% · 장비 누적 " + ( hs ! = null ? hs . gearTotal : 0 ) + "개 +" + st . gearPct . ToString ( "0.#" ) + "%\n" +
"진화 " + ( hs ! = null ? "★" + hs . star : "—" ) + " +" + st . starPct . ToString ( "0.#" ) + "%\n" +
2026-11-05 15:22:54 +00:00
"훈련(" + TrainingRules . RankName ( S . Db , TrainingRules . Rank ( S . Save , h . type ) ) + ") HP +" + st . trainHpPct . ToString ( "0.#" ) + "% · 공 +" + st . trainPct . ToString ( "0.#" ) + "% · 방 +" + st . trainDefPct . ToString ( "0.#" ) + "%\n" +
"보물(수집품·세트) HP +" + st . treasureHpPct . ToString ( "0.#" ) + "% · 공 +" + st . treasureAtkPct . ToString ( "0.#" ) + "% · 방 +" + st . treasureDefPct . ToString ( "0.#" ) + "%\n\n" +
2026-11-05 12:11:10 +00:00
"<size=40>HP " + st . Hp + " · 공 " + st . Atk + " · 방 " + st . Def + "</size>\n" +
"전투력 " + st . power . ToString ( "N0" ) + " = HP/" + e . PowerHpDiv + " + 공×" + e . PowerAtkW + " + 방×" + e . PowerDefW ;
2026-11-09 02:11:20 +00:00
Root . Modal ( "상세정보 — " + h . nameKo , body ,
new OutGameRoot . ModalButton ( "도감" , UiKit . Dark , ( ) = > ShowCodex ( Root ) ) ,
new OutGameRoot . ModalButton ( "닫기" , UiKit . Dark , null ) ) ;
2026-11-05 12:11:10 +00:00
}
/// <summary>도감 = 전 영웅·병사(미보유 회색).</summary>
public static void ShowCodex ( OutGameRoot root )
{
var s = root . Session ;
int owned = 0 ;
foreach ( var h in s . Db . Heroes . heroes )
if ( s . Save . Owns ( h . id ) ) owned + + ;
var content = root . ModalCustom ( "도감 " + owned + " / " + s . Db . Heroes . heroes . Count , 1560f , new OutGameRoot . ModalButton ( "닫기" , UiKit . Dark , null ) ) ;
ScrollRect sr ;
var grid = UiKit . ScrollGrid ( "Codex" , content , 0f , 0f , 0f , 0f , new Vector2 ( 420f , 140f ) , new Vector2 ( 20f , 14f ) , 2 , out sr ) ;
foreach ( var h in s . Db . Heroes . heroes )
{
bool own = s . Save . Owns ( h . id ) ;
var hero = h ;
2026-11-10 01:49:27 +00:00
HeroCards . Card ( grid , s , h , false , own , ( ) = > root . Toast ( CollectionCards . InfoText ( hero , own ) ) ) ;
2026-11-05 12:11:10 +00:00
}
}
}
}