2026-11-05 12:11:10 +00:00
using System.Collections.Generic ;
using UnityEngine ;
using UnityEngine.EventSystems ;
using UnityEngine.UI ;
namespace TKS.OutGame
{
/// <summary>
2026-11-09 04:56:07 +00:00
/// 편성(img08 · 기획 S12 v3) — 프리셋 3 · 전열 3(윗줄)·후열 2(아랫줄) 자리 = 영웅 목록 카드(축소) · 하단 보유 목록(장수 ▼ 전환 · 같은 카드 4열 · 출전) ·
2026-11-05 12:11:10 +00:00
/// 탭(자리 고르기 → 카드 탭 = 넣기 · 같은 자리 두 번 = 빼기 · 다른 자리 = 맞바꾸기) · 끌기(자리 ↔ 자리 · 카드 → 자리) · 저장 = 그 프리셋을 출전 편성으로.
/// 빈 칸 허용 · 최소 1 · 편성 제약 없음(D4).
/// </summary>
public class FormationScreen : OutGameScreen
{
int preset ;
readonly List < string > work = new List < string > ( ) ;
bool dirty ;
int selected = - 1 ;
bool soldiers ;
readonly List < Button > presetBtns = new List < Button > ( ) ;
readonly List < RectTransform > slots = new List < RectTransform > ( ) ;
RectTransform list ;
ScrollRect scroll ;
RectTransform ghost ;
public IList < string > Working
{
get { return work ; }
}
public int Preset
{
get { return preset ; }
}
public bool Dirty
{
get { return dirty ; }
}
2026-11-09 08:07:21 +00:00
// Fable 제안서 §3(2026-11-09 · PD 「편성 메뉴와 영웅 목록 프레임 구분 · 디자인 요소 · 카드 목록 완성도」) — 본문 좌표 = 절대 y − 224
// 섹션 A 「출전 편성」 패널(절대 236~1010) · 홈 16 · 섹션 B 「보유 영웅」 패널(1026~1760) · 자리 = 목록 카드 × SeatScale · 보유 = × ListScale
const float SeatScale = 0.62f ;
2026-11-09 04:56:07 +00:00
const float ListScale = 0.72f ;
2026-11-09 08:16:44 +00:00
const int GhostMin = 8 ; // 보유 격자 최소 칸(2줄 · 빈 칸 = 고스트 카드)
2026-11-09 08:07:21 +00:00
const float SeatPitch = 222f ; // 자리 중심 간격(198 + 24)
const float SeatCenterX = 608f ; // 자리 줄 가운데(절대 x · 왼쪽 라벨 칸만큼 오른쪽)
const float PanelX = 24f , PanelW = 1032f ;
2026-11-09 08:16:44 +00:00
const float PanelA_Y = 12f , PanelA_H = 760f ; // 절대 236~996(Fable 검수 결함 4)
2026-11-09 09:29:50 +00:00
const float PanelB_Y = 804f , PanelB_H = 708f ; // 절대 1028~1736(Fable 재검수 결함 4 · 그림자 아래 탭 바까지 바탕 틈 ≈ 29 · PD 2026-11-09 「고쳐」)
const float GridBottomAbs = 1730f ; // 격자 아래 끝(절대 · B 필드 안쪽 1730)
2026-11-09 08:16:44 +00:00
const float FrontY = 182f , BackY = 471f ; // 자리 위 끝(패널 A 기준 · 밴드 412~691 / 701~980 가운데)
2026-11-09 04:56:07 +00:00
Text countText ;
Text filterText ;
2026-11-09 08:07:21 +00:00
Text ownedCount ;
Text powerText ;
Image saveImg ;
Text saveText ;
Image saveDot ;
readonly List < GameObject > presetTails = new List < GameObject > ( ) ;
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 ) ) ;
}
2026-11-09 04:56:07 +00:00
2026-11-05 12:11:10 +00:00
public override void Build ( )
{
int n = Root . Battle . Config . playerSpawnPoints . Length ;
int front = Mathf . Clamp ( S . Db . Economy . FormationFront , 0 , n ) ;
2026-11-09 04:56:07 +00:00
// 바탕 = 영웅 목록과 같음(진남 + 재화 바 뒤 띠) · 헤더 오른쪽 사선 칸 = 출전 n / 칸 수
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 ) ;
2026-11-09 08:07:21 +00:00
var head = Header ( "편성" , "" , true ) ;
2026-11-09 04:56:07 +00:00
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 , "" , 40 , 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 ;
2026-11-09 08:07:21 +00:00
// 본문 = 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 ) ) ;
// ===== 섹션 A 「출전 편성」 =====
RectTransform a ;
UiKit . SectionPanel ( body , "PanelA" , PanelX , PanelA_Y , PanelW , PanelA_H , true , true , out a ) ;
UiKit . SectionTitle ( a , "출전 편성" ) ;
// 제약 칩(ⓘ = 편성 안내 팝업 · 안내 문장 이관)
2026-11-09 08:16:44 +00:00
var lim = At ( a , "Limit" , 592f , 12f , 230f , 56f ) ;
2026-11-09 08:07:21 +00:00
UiKit . Skin ( lim , "chip" , UiKit . Col ( "chip_dark" , UiKit . Dark ) , true ) ;
UiKit . Layer ( lim , "Line" , "chip_line" , UiKit . Col ( "chip_line" , Color . clear ) , 4f , 4f , 4f , 4f ) ;
UiKit . Label ( lim , "편성 제약 없음" , 26 , UiKit . Col ( "text_soft" , UiKit . InkSoft ) , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 18f , 0f ) , new Vector2 ( 170f , 50f ) ) . horizontalOverflow = HorizontalWrapMode . Overflow ;
UiKit . Icon ( lim , "info" , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 16f , 0f ) , 32f ) ;
lim . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ShowGuide ) ;
// 저장(녹 · 바뀐 것 없으면 비활성 색 · 바뀌면 알림 점)
2026-11-09 08:16:44 +00:00
var sv = At ( a , "Save" , 832f , 14f , 180f , 56f ) ;
2026-11-09 08:07:21 +00:00
saveImg = UiKit . Skin ( sv , UISkin . Has ( "btn_s_green" ) ? "btn_s_green" : "button" , UiKit . White , true ) ;
sv . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( SaveNow ) ;
2026-11-09 08:16:44 +00:00
saveText = UiKit . Title ( UiKit . Label ( sv , "저장" , 34 , UiKit . Col ( "text_on_green" , UiKit . Dark ) , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , 3f ) , new Vector2 ( 170f , 50f ) ) ) ;
saveDot = UiKit . Icon ( sv , "new_dot" , new Vector2 ( 1f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( - 6f , - 6f ) , 20f ) ;
2026-11-09 08:07:21 +00:00
// 프리셋 탭 3 + 출전 꼬리 칩
2026-11-05 12:11:10 +00:00
for ( int i = 0 ; i < S . Save . presets . Count ; i + + )
{
int k = i ;
2026-11-09 08:16:44 +00:00
var b = SubTab ( "Preset" + i , a , "프리셋 " + ( i + 1 ) , new Vector2 ( 20f + i * 212f , - 92f ) , new Vector2 ( 200f , 72f ) , ( ) = > LoadPreset ( k ) ) ;
2026-11-09 08:07:21 +00:00
presetBtns . Add ( b ) ;
2026-11-09 08:16:44 +00:00
var tail = UiKit . Skin ( UiKit . Rect ( "Active" , b . transform , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( - 8f , 4f ) , new Vector2 ( 64f , 30f ) ) , "tail" , UiKit . Col ( "tail_red" , UiKit . Red ) ) ;
UiKit . Title ( UiKit . FillLabel ( tail . transform , "출전" , 18 , UiKit . White ) , true ) ;
2026-11-09 08:07:21 +00:00
presetTails . Add ( tail . gameObject ) ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 08:07:21 +00:00
// 총 전투력 칩(편성 변화가 숫자로 바로 보임)
2026-11-09 08:16:44 +00:00
var pw = At ( a , "Power" , 736f , 96f , 276f , 64f ) ;
2026-11-09 08:07:21 +00:00
UiKit . Skin ( pw , "chip" , UiKit . Col ( "chip_dark" , UiKit . Dark ) ) ;
UiKit . Icon ( pw , "sword" , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 12f , 0f ) , 44f ) ;
UiKit . Label ( pw , "전투력" , 24 , UiKit . Col ( "text_soft" , UiKit . InkSoft ) , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 62f , 0f ) , new Vector2 ( 90f , 40f ) ) ;
powerText = UiKit . Title ( UiKit . Label ( pw , "" , 40 , UiKit . White , TextAnchor . MiddleRight , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 18f , 2f ) , new Vector2 ( 150f , 56f ) ) , true ) ;
powerText . horizontalOverflow = HorizontalWrapMode . Overflow ;
// 열 밴드 2 · 구분선 · 전열/후열 칩
2026-11-09 08:16:44 +00:00
UiKit . Skin ( At ( a , "FrontBand" , 20f , 176f , 992f , 279f ) , "row_band" , UiKit . Col ( "row_band" , Color . clear ) ) ;
UiKit . Skin ( At ( a , "Divider" , 76f , 457f , 880f , 8f ) , "divider" , UiKit . Col ( "divider" , Color . clear ) ) ;
UiKit . Skin ( At ( a , "BackBand" , 20f , 465f , 992f , 279f ) , "row_band" , UiKit . Col ( "row_band" , Color . clear ) ) ;
// 라벨 칸 세로 띠(라벨 쪽을 면으로 분리 · 검수 결함 7)
UiKit . Skin ( At ( a , "FrontStrip" , 20f , 176f , 156f , 279f ) , "row_band" , UiKit . Col ( "row_label_strip" , Color . clear ) ) ;
UiKit . Skin ( At ( a , "BackStrip" , 20f , 465f , 156f , 279f ) , "row_band" , UiKit . Col ( "row_label_strip" , Color . clear ) ) ;
Pill ( a , "전열" , UiKit . Col ( "row_front" , UiKit . Red ) , 16f , 176f + 139.5f - 30f ) ;
Pill ( a , "후열" , UiKit . Col ( "row_back" , UiKit . Dark ) , 16f , 465f + 139.5f - 30f ) ;
2026-11-09 08:07:21 +00:00
// 자리 = 받침(빈 자리에도) + 카드 칸
2026-11-09 04:56:07 +00:00
float sw = CollectionCards . CellSize . x * SeatScale , sh = CollectionCards . CellSize . y * SeatScale ;
2026-11-05 12:11:10 +00:00
for ( int i = 0 ; i < n ; i + + )
{
bool f = i < front ;
int col = f ? i : i - front ;
int cnt = f ? front : n - front ;
2026-11-09 08:07:21 +00:00
float cx = SeatCenterX - PanelX + ( col - ( cnt - 1 ) / 2f ) * SeatPitch ;
float top = f ? FrontY : BackY ;
var plate = At ( a , "Plate" + i , cx - sw / 2f - 8f , top - 8f , sw + 16f , sh + 16f ) ;
UiKit . Skin ( plate , "seat_plate" , UiKit . Col ( "seat_plate" , UiKit . Dark ) ) ;
var pl = UiKit . Rect ( "Light" , plate , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 4f ) , new Vector2 ( sw , 10f ) ) ;
UiKit . Skin ( pl , "panel_light" , UiKit . Col ( "seat_light" , Color . clear ) ) . enabled = UISkin . Has ( "panel_light" ) ;
var s = At ( a , "Slot" + i , cx - sw / 2f , top , sw , sh ) ;
2026-11-05 12:11:10 +00:00
var hit = s . gameObject . AddComponent < Image > ( ) ;
2026-11-09 04:56:07 +00:00
hit . color = Color . clear ;
2026-11-05 12:11:10 +00:00
hit . raycastTarget = true ;
var td = s . gameObject . AddComponent < TapDrag > ( ) ;
td . AnyDirection = true ;
int k = i ;
td . OnTap = ( ) = > TapSlot ( k ) ;
td . OnDragStart = e = > StartGhost ( work [ k ] , e ) ;
td . OnDragMove = MoveGhost ;
td . OnDragEnd = e = > DropFromSlot ( k , e ) ;
slots . Add ( s ) ;
}
2026-11-09 08:07:21 +00:00
// ===== 섹션 B 「보유 영웅」 =====
RectTransform b2 ;
UiKit . SectionPanel ( body , "PanelB" , PanelX , PanelB_Y , PanelW , PanelB_H , false , false , out b2 ) ;
var tb = UiKit . SectionTitle ( b2 , "보유 영웅" ) ;
var cnt2 = UiKit . Skin ( UiKit . Rect ( "Count" , tb , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 300f , 2f ) , new Vector2 ( 44f , 46f ) ) , "badge_circle" , UiKit . Col ( "grade_blue" , UiKit . Dark ) ) ;
cnt2 . preserveAspect = true ;
ownedCount = UiKit . Title ( UiKit . FillLabel ( cnt2 . transform , "" , 26 , UiKit . White ) , true ) ;
// 「장수 ▼」 필터 칩
var pick = At ( b2 , "Filter" , 812f , 6f , 200f , 56f ) ;
UiKit . Skin ( pick , "chip" , UiKit . Col ( "chip_dark" , UiKit . Dark ) , true ) ;
UiKit . Layer ( pick , "Line" , "chip_line" , UiKit . Col ( "chip_line" , Color . clear ) , 4f , 4f , 4f , 4f ) ;
2026-11-09 04:56:07 +00:00
pick . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ( ) = >
{
soldiers = ! soldiers ;
Refresh ( ) ;
} ) ;
2026-11-09 08:07:21 +00:00
filterText = UiKit . Title ( UiKit . Label ( pick , "" , 30 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( - 18f , 2f ) , new Vector2 ( 140f , 50f ) ) ) ;
UiKit . Icon ( pick , "arrow_down" , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 22f , 0f ) , 28f ) ;
2026-11-09 09:29:50 +00:00
// 격자 4열(절대 x 54~1026 · y 1108~1730) + 위·아래 페이드
2026-11-09 04:56:07 +00:00
var cell = CollectionCards . CellSize * ListScale ;
2026-11-09 09:29:50 +00:00
float gridTop = 1110f - 224f , gridBottom = OutGameRoot . BodyH - ( GridBottomAbs - 224f ) ;
2026-11-09 08:07:21 +00:00
list = UiKit . ScrollGrid ( "List" , body , 54f , 54f , gridTop , gridBottom , cell , new Vector2 ( 17f , 16f ) , 4 , out scroll ) ;
UiKit . ScrollFades ( body , gridTop , gridBottom , 40f , 30f ) ;
}
/// <summary>편성 안내(제약 칩 ⓘ · 화면의 안내 문장을 이관).</summary>
void ShowGuide ( )
{
Root . Modal ( "편성 안내" , "편성 제약 없음 · 빈 칸 허용 · 최소 1명\n\n자리 탭 → 카드 탭 = 넣기 · 교체\n같은 자리 두 번 탭 = 빼기\n끌기 = 자리 ↔ 자리 · 카드 → 자리\n저장 = 이 프리셋을 출전 편성으로" , new OutGameRoot . ModalButton ( "확인" , UiKit . Dark , null ) ) ;
2026-11-09 04:56:07 +00:00
}
/// <summary>서브탭(팩 Button_Round10 + 윗면 광택 · 선택 = 붉은 옻칠).</summary>
static Button SubTab ( string name , Transform parent , string label , Vector2 pos , Vector2 size , System . Action onClick )
{
var img = UiKit . Skin ( UiKit . Rect ( name , parent , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , pos , size ) , UISkin . Has ( "subtab" ) ? "subtab" : "button" , UiKit . Col ( "subtab_off" , UiKit . Paper ) , true ) ;
var b = img . gameObject . AddComponent < Button > ( ) ;
b . onClick . AddListener ( ( ) = > onClick ( ) ) ;
2026-11-09 08:07:21 +00:00
UiKit . Title ( UiKit . FillLabel ( img . transform , label , 32 , UiKit . White , TextAnchor . MiddleCenter , true , 8f ) , true ) ;
2026-11-09 04:56:07 +00:00
return b ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 08:07:21 +00:00
static void Pill ( RectTransform panel , string label , Color c , float x , float y )
2026-11-05 12:11:10 +00:00
{
2026-11-09 08:07:21 +00:00
// 전열·후열 라벨 = 평행사변형 칩 120× 54(Fable §3-3)
2026-11-09 03:24:04 +00:00
var p = UISkin . Has ( "para_orange" )
2026-11-09 08:16:44 +00:00
? UiKit . Skin ( At ( panel , "Row_" + label , x , y , 140f , 60f ) , "para_orange" , c )
: UiKit . Paint ( At ( panel , "Row_" + label , x , y , 140f , 60f ) , c ) ;
UiKit . Title ( UiKit . FillLabel ( p . transform , label , 30 , UiKit . White ) , true ) ;
2026-11-05 12:11:10 +00:00
}
public override void OnShow ( )
{
LoadPreset ( S . Save . presetIdx , true ) ;
}
public override bool OnBack ( )
{
if ( dirty ) Root . Toast ( "저장하지 않은 편성 변경은 취소됩니다" ) ;
dirty = false ;
return false ;
}
public void LoadPreset ( int idx , bool quiet = false )
{
if ( dirty & & ! quiet ) Root . Toast ( "저장하지 않은 변경 취소" ) ;
preset = Mathf . Clamp ( idx , 0 , S . Save . presets . Count - 1 ) ;
work . Clear ( ) ;
work . AddRange ( S . Save . presets [ preset ] . slots ) ;
dirty = false ;
selected = - 1 ;
Refresh ( ) ;
}
int Filled ( )
{
int n = 0 ;
foreach ( var id in work )
if ( ! string . IsNullOrEmpty ( id ) ) n + + ;
return n ;
}
public void TapSlot ( int i )
{
if ( selected < 0 )
{
selected = i ;
}
else if ( selected = = i )
{
if ( ! string . IsNullOrEmpty ( work [ i ] ) )
{
if ( Filled ( ) > 1 )
{
work [ i ] = "" ;
dirty = true ;
}
else Root . Toast ( "최소 1명은 출전해야 합니다" ) ;
}
selected = - 1 ;
}
else
{
Swap ( selected , i ) ;
selected = - 1 ;
}
Refresh ( ) ;
}
public void TapCard ( string id )
{
int at = work . IndexOf ( id ) ;
if ( at > = 0 )
{
if ( Filled ( ) > 1 )
{
work [ at ] = "" ;
dirty = true ;
}
else Root . Toast ( "최소 1명은 출전해야 합니다" ) ;
}
else
{
int target = selected > = 0 ? selected : work . IndexOf ( "" ) ;
if ( target < 0 ) Root . Toast ( "빈 자리가 없습니다 — 자리를 먼저 탭해 고른 뒤 카드를 탭(교체)" ) ;
else
{
work [ target ] = id ;
dirty = true ;
}
}
selected = - 1 ;
Refresh ( ) ;
}
/// <summary>id 를 자리 slot 에 넣는다(이미 다른 자리에 있으면 맞바꿈).</summary>
public void Place ( string id , int slot )
{
if ( slot < 0 | | slot > = work . Count | | string . IsNullOrEmpty ( id ) ) return ;
int at = work . IndexOf ( id ) ;
if ( at > = 0 ) Swap ( at , slot ) ;
else
{
work [ slot ] = id ;
dirty = true ;
}
Refresh ( ) ;
}
public void Swap ( int a , int b )
{
if ( a = = b ) return ;
var t = work [ a ] ;
work [ a ] = work [ b ] ;
work [ b ] = t ;
dirty = true ;
}
public void SaveNow ( )
{
var why = S . SavePreset ( preset , work ) ;
if ( why ! = null )
{
Root . Toast ( why ) ;
return ;
}
dirty = false ;
Root . Toast ( "프리셋 " + ( preset + 1 ) + " 저장 · 출전 편성으로 지정" ) ;
Refresh ( ) ;
}
// ---------- 끌기 ----------
void StartGhost ( string id , PointerEventData e )
{
if ( string . IsNullOrEmpty ( id ) ) return ;
if ( ghost ! = null ) Object . Destroy ( ghost . gameObject ) ;
ghost = UiKit . Portrait ( "Ghost" , Root . TopRoot , S . Def ( id ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , new Vector2 ( 170f , 170f ) , true , 2.1f , UiKit . Red ) ;
ghost . gameObject . name = "Ghost_" + id ;
2026-11-09 16:05:47 +00:00
UiLayer . Nest ( ghost . gameObject , UiRate . Frame ) ; // 끄는 동안 매 프레임 이동 = 자기 캔버스(PD 2026-11-10)
2026-11-05 12:11:10 +00:00
MoveGhost ( e ) ;
}
void MoveGhost ( PointerEventData e )
{
if ( ghost = = null ) return ;
Vector2 lp ;
if ( RectTransformUtility . ScreenPointToLocalPointInRectangle ( Root . TopRoot , e . position , Root . Cam , out lp ) ) ghost . anchoredPosition = lp ;
}
int SlotAt ( Vector2 screen )
{
for ( int i = 0 ; i < slots . Count ; i + + )
if ( RectTransformUtility . RectangleContainsScreenPoint ( slots [ i ] , screen , Root . Cam ) ) return i ;
return - 1 ;
}
void DropFromSlot ( int from , PointerEventData e )
{
EndGhost ( ) ;
int to = SlotAt ( e . position ) ;
if ( to < 0 | | to = = from ) return ;
Swap ( from , to ) ;
selected = - 1 ;
Refresh ( ) ;
}
void DropCard ( string id , PointerEventData e )
{
EndGhost ( ) ;
int to = SlotAt ( e . position ) ;
if ( to > = 0 ) Place ( id , to ) ;
}
void EndGhost ( )
{
if ( ghost = = null ) return ;
ghost . gameObject . SetActive ( false ) ;
Object . Destroy ( ghost . gameObject ) ;
ghost = null ;
}
// ---------- 그리기 ----------
public override void Refresh ( )
{
for ( int i = 0 ; i < presetBtns . Count ; i + + )
{
bool on = i = = preset ;
2026-11-09 04:56:07 +00:00
presetBtns [ i ] . GetComponent < Image > ( ) . color = on ? UiKit . Red : UiKit . Col ( "subtab_off" , UiKit . Paper ) ;
2026-11-05 12:11:10 +00:00
var t = UiKit . LabelOf ( presetBtns [ i ] ) ;
2026-11-09 04:56:07 +00:00
t . color = UiKit . White ;
2026-11-09 08:07:21 +00:00
t . text = "프리셋 " + ( i + 1 ) + ( on & & dirty ? "*" : "" ) ;
presetTails [ i ] . SetActive ( i = = S . Save . presetIdx ) ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 04:56:07 +00:00
countText . text = "출전 " + Filled ( ) + "/" + work . Count ;
2026-11-09 08:07:21 +00:00
float power = 0f ;
foreach ( var id in work )
if ( ! string . IsNullOrEmpty ( id ) & & S . Def ( id ) ! = null ) power + = S . StatsOf ( id ) . power ;
powerText . text = power . ToString ( "N0" ) ;
// 저장 = 바뀐 것이 있을 때만 녹(+ 알림 점)
2026-11-09 08:16:44 +00:00
// 비활성 = 회색 스프라이트(녹에 회색을 곱하면 녹이 남음 · 검수 결함 3) · 광택 없음 · 글자 #5A5E70
UISkin . Apply ( saveImg , dirty | | ! UISkin . Has ( "btn_s_gray" ) ? "btn_s_green" : "btn_s_gray" ) ;
saveImg . color = UiKit . White ;
2026-11-09 08:07:21 +00:00
saveText . color = UiKit . Col ( dirty ? "text_on_green" : "disabled_text" , UiKit . Dark ) ;
if ( saveDot ! = null ) saveDot . gameObject . SetActive ( dirty ) ;
// 자리 = 목록 카드(배율) · 빈 자리 = 원본 빈 카드 + 초록 플러스 뱃지 · 고른 자리 = 노란 포커스 테두리(깜빡)
2026-11-05 12:11:10 +00:00
for ( int i = 0 ; i < slots . Count ; i + + )
{
var s = slots [ i ] ;
UiKit . Clear ( s ) ;
var id = i < work . Count ? work [ i ] : "" ;
var h = S . Def ( id ) ;
2026-11-09 04:56:07 +00:00
if ( h ! = null ) CollectionCards . CardIn ( s , S , h , false , null , SeatScale ) ;
2026-11-05 12:11:10 +00:00
else
{
2026-11-09 04:56:07 +00:00
CollectionCards . EmptyIn ( s , SeatScale ) ;
2026-11-09 08:07:21 +00:00
var badge = UiKit . Skin ( UiKit . Rect ( "Plus" , s , new Vector2 ( 1f , 0f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( - 24f , 24f ) , new Vector2 ( 48f , 50f ) ) , "badge_circle" , UiKit . Col ( "ok" , UiKit . Red ) ) ;
badge . preserveAspect = true ;
UiKit . Icon ( badge . transform , "plus_glyph" , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , 1f ) , 26f ) ;
}
if ( i = = selected )
{
var fx = UiKit . Layer ( s , "Select" , "focus_line" , UiKit . Col ( "select_line" , UiKit . Star ) , - 6f , - 6f , - 6f , - 6f ) ;
fx . gameObject . AddComponent < UiPulse > ( ) ;
2026-11-05 12:11:10 +00:00
}
}
2026-11-09 08:07:21 +00:00
// 보유 목록 = 영웅 목록 카드(배율 · 레벨 높은 순) · 출전 중 = 꼬리 칩 + 35 % 덮개 + 초록 체크 뱃지
2026-11-09 04:56:07 +00:00
filterText . text = soldiers ? "병사" : "장수" ;
2026-11-05 12:11:10 +00:00
UiKit . Clear ( list ) ;
2026-11-09 04:56:07 +00:00
var owned = new List < HeroDb . Hero > ( S . Owned ( soldiers ) ) ;
2026-11-09 08:07:21 +00:00
ownedCount . text = owned . Count . ToString ( ) ;
2026-11-09 04:56:07 +00:00
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 ;
return lb . CompareTo ( la ) ;
} ) ;
var cellSize = CollectionCards . CellSize * ListScale ;
foreach ( var h in owned )
2026-11-05 12:11:10 +00:00
{
var id = h . id ;
2026-11-09 08:07:21 +00:00
bool on = work . Contains ( id ) ;
2026-11-09 04:56:07 +00:00
var holder = UiKit . Rect ( "C_" + id , list , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , cellSize ) ;
2026-11-09 08:07:21 +00:00
var card = CollectionCards . CardIn ( holder , S , h , on , null , ListScale ) ;
if ( on )
{
2026-11-09 08:16:44 +00:00
var dim = UiKit . Layer ( card . transform , "Dim" , "card_bg" , UiKit . Col ( "seat_dim" , Color . clear ) , 2f , 80f , 2f , 70f ) ; // 초상만(이름·레벨·별 제외)
2026-11-09 08:07:21 +00:00
dim . transform . SetSiblingIndex ( 2 ) ; // 바탕·테두리 위 · 글자·뱃지 아래
2026-11-09 08:16:44 +00:00
var ck = UiKit . Skin ( UiKit . Rect ( "Check" , card . transform , new Vector2 ( 1f , 0f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( - 47f , 86f ) , new Vector2 ( 67f , 70f ) ) , "check_circle" , UiKit . Col ( "ok" , UiKit . Red ) ) ;
2026-11-09 08:07:21 +00:00
ck . preserveAspect = true ;
2026-11-09 08:16:44 +00:00
UiKit . Icon ( ck . transform , "check_badge" , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , 2f ) , 39f ) ;
2026-11-09 08:07:21 +00:00
}
2026-11-05 12:11:10 +00:00
var td = card . gameObject . AddComponent < TapDrag > ( ) ;
td . OnTap = ( ) = > TapCard ( id ) ;
td . OnDragStart = e = > StartGhost ( id , e ) ;
td . OnDragMove = MoveGhost ;
td . OnDragEnd = e = > DropCard ( id , e ) ;
}
2026-11-09 08:16:44 +00:00
// 빈 칸 = 고스트 카드(영웅 목록과 같은 빈 카드)로 최소 2줄(8칸)을 채움(Fable 검수 결함 5)
for ( int i = owned . Count ; i < GhostMin ; i + + )
{
var holder = UiKit . Rect ( "Ghost" + i , list , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , cellSize ) ;
CollectionCards . EmptyIn ( holder , ListScale ) . raycastTarget = false ;
}
2026-11-05 12:11:10 +00:00
}
}
}