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 04:56:07 +00:00
// 자리 카드 = 영웅 목록 카드(320× 431) × SeatScale · 보유 목록 카드 × ListScale(PD 2026-11-09 「변경된 영웅 리스트 반영 · 편성 유닛 정보를 목록·상세 UI 참고해 깔끔하게」)
const float SeatScale = 0.70f ;
const float ListScale = 0.72f ;
const float SeatGap = 24f ;
const float BoardTop = 122f ;
const float BoardH = 680f ;
Text countText ;
Text filterText ;
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 ) ;
var head = Header ( "편성" , "" , true ) ; // 부제 삭제(설계서 §2-5)
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-05 12:11:10 +00:00
var body = Body ( ) ;
2026-11-09 04:56:07 +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 04:56:07 +00:00
presetBtns . Add ( SubTab ( "Preset" + i , body , "프리셋 " + ( i + 1 ) , new Vector2 ( 24f + i * 226f , - 18f ) , new Vector2 ( 210f , 84f ) , ( ) = > LoadPreset ( k ) ) ) ;
2026-11-05 12:11:10 +00:00
}
2026-11-09 04:56:07 +00:00
var chip = UiKit . Skin ( UiKit . Rect ( "Free" , body , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( - 24f , - 24f ) , new Vector2 ( 270f , 72f ) ) , "chip" , UiKit . Col ( "chip_dark" , UiKit . Dark ) ) ;
UiKit . Title ( UiKit . FillLabel ( chip . transform , "편성 제약 없음" , 30 , UiKit . White ) , false ) ;
// 자리 판 = 전열(윗줄) · 후열(아랫줄) · 자리 = 목록과 같은 카드
float sw = CollectionCards . CellSize . x * SeatScale , sh = CollectionCards . CellSize . y * SeatScale ;
var board = UiKit . TopBand ( "Board" , body , BoardTop , BoardH , 24f , 24f ) ;
UiKit . Skin ( board , "card" , UiKit . Col ( "formation_field" , UiKit . Paper ) ) ;
Pill ( board , "전열" , UiKit . Col ( "row_front" , UiKit . Red ) , - ( 20f + sh / 2f ) ) ;
Pill ( board , "후열" , UiKit . Col ( "row_back" , UiKit . Dark ) , - ( 20f + sh + 20f + sh / 2f ) ) ;
UiKit . Label ( board , "탭·드래그 = 자리 교체 · 같은 자리 두 번 탭 = 빼기" , 24 , UiKit . InkSoft , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 24f , 2f ) , new Vector2 ( 900f , 34f ) , false ) ;
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 04:56:07 +00:00
float x = 60f + ( col - ( cnt - 1 ) / 2f ) * ( sw + SeatGap ) ;
float y = f ? - 20f : - ( 20f + sh + 20f ) ;
var s = UiKit . Rect ( "Slot" + i , board , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( x , y ) , new Vector2 ( 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 04:56:07 +00:00
// 보유 목록 머리 = 제목 · 「장수 ▼」(영웅 목록과 같은 전환) · 저장
var row = UiKit . TopBand ( "ListHead" , body , BoardTop + BoardH + 16f , 74f , 24f , 24f ) ;
UiKit . Title ( UiKit . Label ( row , "보유 영웅" , 40 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 4f , 0f ) , new Vector2 ( 300f , 70f ) ) , true ) ;
var pick = UiKit . Rect ( "Filter" , row , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 244f , 0f ) , new Vector2 ( 220f , 74f ) ) ;
pick . gameObject . AddComponent < Image > ( ) . color = Color . clear ;
pick . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ( ) = >
{
soldiers = ! soldiers ;
Refresh ( ) ;
} ) ;
filterText = UiKit . Title ( UiKit . Label ( pick , "" , 38 , UiKit . InkSoft , TextAnchor . MiddleRight , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 50f , 2f ) , new Vector2 ( 160f , 70f ) ) ) ;
UiKit . DropShadow ( UiKit . Icon ( pick , "arrow_down" , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 6f , 0f ) , 38f ) , 3f ) ;
UiKit . Btn ( "Save" , row , "저장" , 34 , UiKit . Red , UiKit . White , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , Vector2 . zero , new Vector2 ( 210f , 76f ) , SaveNow ) ;
var cell = CollectionCards . CellSize * ListScale ;
float side = ( OutGameRoot . Reference . x - cell . x * 4f - 17f * 3f ) / 2f ;
list = UiKit . ScrollGrid ( "List" , body , side , side , BoardTop + BoardH + 100f , 0f , cell , new Vector2 ( 17f , 16f ) , 4 , out scroll ) ;
}
/// <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 ( ) ) ;
UiKit . Title ( UiKit . FillLabel ( img . transform , label , 34 , UiKit . White , TextAnchor . MiddleCenter , true , 8f ) , true ) ;
return b ;
2026-11-05 12:11:10 +00:00
}
static void Pill ( RectTransform board , string label , Color c , float y )
{
2026-11-09 04:56:07 +00:00
// 전열·후열 라벨 = 평행사변형 칩(설계서 §3③)
2026-11-09 03:24:04 +00:00
var p = UISkin . Has ( "para_orange" )
? UiKit . Skin ( UiKit . Rect ( "Row_" + label , board , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 20f , y ) , new Vector2 ( 140f , 62f ) ) , "para_orange" , c )
: UiKit . Box ( "Row_" + label , board , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 20f , y ) , new Vector2 ( 140f , 62f ) , c ) ;
UiKit . Title ( UiKit . FillLabel ( p . transform , label , 32 , 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 ;
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-05 12:11:10 +00:00
t . text = "프리셋 " + ( i + 1 ) + ( i = = S . Save . presetIdx ? " <size=22>출전</size>" : "" ) + ( on & & dirty ? "*" : "" ) ;
}
2026-11-09 04:56:07 +00:00
countText . text = "출전 " + Filled ( ) + "/" + work . Count ;
// 자리 = 목록과 같은 카드(배율) · 빈 자리 = 빈 카드 · 고른 자리 = 노란 테두리
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 ( i = = selected ) UiKit . Layer ( s , "Select" , "card_border" , UiKit . Col ( "select_line" , UiKit . Star ) , - 7f , - 7f , - 7f , - 7f ) ;
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 ) ;
UiKit . Title ( UiKit . Label ( s , "+ 빈 자리" , 28 , UiKit . InkSoft , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , 24f ) , new Vector2 ( 200f , 50f ) ) ) ;
2026-11-05 12:11:10 +00:00
}
}
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 ) ) ;
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 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 ) ;
var card = CollectionCards . CardIn ( holder , S , h , work . Contains ( id ) , null , ListScale ) ;
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 ) ;
}
}
}
}