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 배지 · 세력 깃발 · (출전 꼬리 칩)
var ring = UiKit . Skin ( UiKit . Rect ( "Type" , t , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 142f , - 12f ) , new Vector2 ( 52f , 52f ) ) , "dot" , UiKit . White ) ;
ring . preserveAspect = true ;
var cls = UiKit . Icon ( ring . transform , UiKit . TypeIconId ( h . type ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , 36f ) ;
if ( cls = = null ) UiKit . Title ( UiKit . FillLabel ( ring . transform , UnitTypeInfo . Mark ( h . type ) , 26 , UiKit . White ) , true ) ;
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 ;
}
var flag = UiKit . Icon ( t , UISkin . Icon ( "flag_" + h . faction ) ! = null ? "flag_" + h . faction : "flag_other" , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 264f , - 14f ) , 44f ) ;
UiKit . DropShadow ( flag , 2f ) ;
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 ;
}
}
/// <summary>영웅 탭 = 보유 영웅·병사 목록(장수|병사 · 2줄 카드) → 탭 = 영웅 상세 · 도감.</summary>
public class HeroListScreen : OutGameScreen
{
bool soldiers ;
RectTransform content ;
ScrollRect scroll ;
Button tabHero ;
Button tabSoldier ;
public bool ShowingSoldiers
{
get { return soldiers ; }
}
public override void Build ( )
{
Tab = 0 ;
2026-11-09 03:20:32 +00:00
Header ( "영웅" , "" , true ) ; // 부제 삭제(설계서 §2-5)
2026-11-05 12:11:10 +00:00
var body = Body ( ) ;
tabHero = UiKit . Btn ( "TabHero" , body , "장수" , 36 , UiKit . Red , UiKit . White , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f , - 20f ) , new Vector2 ( 220f , 84f ) , ( ) = > SetFilter ( false ) ) ;
tabSoldier = UiKit . Btn ( "TabSoldier" , body , "병사" , 36 , UiKit . Paper , UiKit . Ink , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 258f , - 20f ) , new Vector2 ( 220f , 84f ) , ( ) = > SetFilter ( true ) ) ;
UiKit . Btn ( "Codex" , body , "도감" , 36 , UiKit . Dark , UiKit . White , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( - 24f , - 20f ) , new Vector2 ( 220f , 84f ) , ( ) = > HeroDetailScreen . ShowCodex ( Root ) ) ;
content = UiKit . ScrollGrid ( "List" , body , 24f , 24f , 124f , 16f , new Vector2 ( 506f , 150f ) , new Vector2 ( 20f , 18f ) , 2 , out scroll ) ;
}
public void SetFilter ( bool soldier )
{
soldiers = soldier ;
Refresh ( ) ;
}
public override void Refresh ( )
{
tabHero . GetComponent < Image > ( ) . color = soldiers ? UiKit . Paper : UiKit . Red ;
UiKit . LabelOf ( tabHero ) . color = soldiers ? UiKit . Ink : UiKit . White ;
tabSoldier . GetComponent < Image > ( ) . color = soldiers ? UiKit . Red : UiKit . Paper ;
UiKit . LabelOf ( tabSoldier ) . color = soldiers ? UiKit . White : UiKit . Ink ;
UiKit . LabelOf ( tabHero ) . text = "장수 " + S . Owned ( false ) . Count ;
UiKit . LabelOf ( tabSoldier ) . text = "병사 " + S . Owned ( true ) . Count ;
UiKit . Clear ( content ) ;
foreach ( var h in S . Owned ( soldiers ) )
{
var id = h . id ;
HeroCards . Card ( content , S , h , S . InActivePreset ( id ) , true , ( ) = > Root . ShowHero ( id ) ) ;
}
}
public int CardCount
{
get { return content . childCount ; }
}
}
/// <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 ;
Image flagIcon ;
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" ) ;
flagIcon = Ico ( body , null , 44f , 789f , 88f , 88f , 3f ) ;
Spr ( body , "TypeRing" , "circle_frame" , 140f , 794f , 78f , 78f , "white" ) ;
Spr ( body , "TypeIn" , "circle_frame" , 144f , 798f , 70f , 70f , "circle_inner" ) ;
clsIcon = Ico ( body , null , 147f , 801f , 64f , 64f ) ;
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 ) ;
skCls [ i ] = Ico ( body , null , 80f , y + 32f , 82f , 82f , 3f ) ;
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 ) ;
// 이름 띠
SetIcon ( flagIcon , UISkin . Icon ( "flag_" + h . faction ) ! = null ? "flag_" + h . faction : "flag_other" , 1f ) ;
SetIcon ( clsIcon , UiKit . TypeIconId ( 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 ) ;
SetIcon ( skCls [ i ] , UiKit . TypeIconId ( h . type ) , has ? 1f : 0.5f ) ;
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-09 03:20:32 +00:00
HeroCards . Card ( grid , s , h , false , own , ( ) = > root . Toast ( hero . nameKo + ( string . IsNullOrEmpty ( hero . hanja ) ? "" : "(" + hero . hanja + ")" ) + " · " +
2026-11-05 12:11:10 +00:00
UnitTypeInfo . Korean ( hero . type ) + " · ★" + hero . rarity + ( hero . isSoldier ? " · 병사 유닛" : "" ) + ( own ? " · 보유" : " · 미보유 — 거래 > 뽑기" ) ) ) ;
}
}
}
}