2026-11-05 15:22:54 +00:00
using System.Collections.Generic ;
using UnityEngine ;
using UnityEngine.UI ;
namespace TKS.OutGame
{
/// <summary>
2026-11-12 13:41:33 +00:00
/// 내정(하단 탭 · 기획 S9~S11 · PD #1093 C) — 상단 탭 3(한 항목 = 한 페이지) · **PD 2026-11-12 내정 메뉴 개편 = 로비처럼 탭마다 일러 배경 장면 + 화풍 통일**:
/// ① 훈련 = 연병장 배경 + 고른 병종의 범용 병사 5명이 한 구령에 훈련(원화 대기·자세·타격 클립 · UiDrillAnim) · 병종 탭 5 · 랭크 진행 · 카드 3(HP·공·방) · 3항목 Max → 랭크 승급
/// ② 순찰 = 저잣거리 배경이 보이게 위(순찰 상태 · 「시간당 수확」 = 누르면 펼치는 드롭다운) · 아래(쌓인 보상 · 버튼 「수확」 「즉시 수확」 2개만) · 광고 2배 = 수확을 누른 뒤 고름
/// ③ 개발 = 규장각 배경 + 양피지 두루마리 위 성장 트리(갈래 ◀ 상업·농업·공업 ▶ · 선행 조건 깊이로 줄 · 노드 탭 = 강화 팝업) · 도면·골드 · 트리 초기화.
/// 배경·연출 = 표 Resources/UI/domestic_scene.csv(DomesticScene) · 연출 층 = LobbyAmbient(Frame 1장 · P41).
2026-11-05 15:22:54 +00:00
/// </summary>
public class DomesticScreen : OutGameScreen
{
public static readonly string [ ] PageNames = { "훈련" , "순찰" , "개발" } ;
2026-11-12 13:41:33 +00:00
class Scene
{
public List < LobbyScene . Row > rows ;
public LobbyAmbient amb ;
public RectTransform art ;
public float artW ;
public float artH ;
}
2026-11-05 15:22:54 +00:00
int page ;
int typeIdx ;
int branchIdx ;
2026-11-12 13:41:33 +00:00
RectTransform backdrop ;
readonly Scene [ ] scenes = new Scene [ 3 ] ;
2026-11-05 15:22:54 +00:00
readonly List < Button > tabs = new List < Button > ( ) ;
readonly List < RectTransform > pages = new List < RectTransform > ( ) ;
public int Page
{
get { return page ; }
}
/// <summary>병종 탭 순서 = training.csv 행 순서(보·창·기·궁·책).</summary>
public List < UnitType > TrainTypes
{
get
{
var l = new List < UnitType > ( ) ;
foreach ( var r in S . Db . Training . rows )
if ( ! l . Contains ( r . type ) ) l . Add ( r . type ) ;
return l ;
}
}
public UnitType TrainType
{
get
{
var l = TrainTypes ;
return l . Count > 0 ? l [ Mathf . Clamp ( typeIdx , 0 , l . Count - 1 ) ] : UnitType . Infantry ;
}
}
public string Branch
{
get
{
var b = S . Db . Dev . Branches ;
return b . Count > 0 ? b [ Mathf . Clamp ( branchIdx , 0 , b . Count - 1 ) ] : "" ;
}
}
/// <summary>개발 페이지에 그려진 노드 수(테스트).</summary>
public int NodeCount { get ; private set ; }
2026-11-12 13:41:33 +00:00
/// <summary>그림 아이콘으로 그린 노드 수(테스트 · 나머지 = 첫 글자).</summary>
public int IconNodeCount { get ; private set ; }
/// <summary>훈련 장면에 선 병사 수(테스트).</summary>
public int SoldierCount { get ; private set ; }
/// <summary>그 탭의 배경 장면 층(없으면 null · 테스트).</summary>
public LobbyAmbient SceneOf ( int p )
{
return p > = 0 & & p < scenes . Length & & scenes [ p ] ! = null ? scenes [ p ] . amb : null ;
}
2026-11-05 15:22:54 +00:00
public override void Build ( )
{
Tab = 2 ;
2026-11-12 13:41:33 +00:00
if ( UISkin . On ) UiKit . Paint ( UiKit . Fill ( "ListBg" , Rt ) , UiKit . Col ( "list_bg" , UiKit . Bg ) , false ) ; // 목록형 바탕(배경 그림이 없을 때)
2026-11-05 15:22:54 +00:00
Header ( "내정" , "훈련 · 순찰 · 개발" , true ) ;
var body = Body ( ) ;
2026-11-12 13:41:33 +00:00
backdrop = UiKit . Fill ( "Backdrop" , body ) ; // 탭마다 배경 장면(처음 열 때 만듦 · 탭·페이지 뒤)
2026-11-05 15:22:54 +00:00
for ( int i = 0 ; i < PageNames . Length ; i + + )
{
int k = i ;
2026-11-09 09:12:53 +00:00
tabs . Add ( UISkin . On
? UiKit . SubTab ( body , "Tab" + i , PageNames [ i ] , 24f + i * 348f , 18f , 332f , 84f , ( ) = > SetPage ( k ) )
: UiKit . Btn ( "Tab" + i , body , PageNames [ i ] , 36 , UiKit . Paper , UiKit . Ink , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f + i * 348f , - 18f ) , new Vector2 ( 332f , 84f ) , ( ) = > SetPage ( k ) ) ) ;
2026-11-05 15:22:54 +00:00
pages . Add ( UiKit . Fill ( "Page" + i , body , 0f , 0f , 118f , 0f ) ) ;
}
}
public void SetPage ( int p )
{
page = Mathf . Clamp ( p , 0 , pages . Count - 1 ) ;
Refresh ( ) ;
}
public void SetTrainType ( UnitType t )
{
typeIdx = Mathf . Max ( 0 , TrainTypes . IndexOf ( t ) ) ;
Refresh ( ) ;
}
public void SetBranch ( int i )
{
int n = Mathf . Max ( 1 , S . Db . Dev . Branches . Count ) ;
branchIdx = ( ( i % n ) + n ) % n ;
Refresh ( ) ;
}
2026-11-12 13:41:33 +00:00
Scene EnsureScene ( int p )
{
if ( scenes [ p ] ! = null ) return scenes [ p ] ;
var s = new Scene { rows = DomesticScene . Page ( DomesticScene . Pages [ p ] ) } ;
s . amb = DomesticScene . Backdrop ( backdrop , "Scene_" + DomesticScene . Pages [ p ] , s . rows , out s . art , out s . artW , out s . artH ) ;
scenes [ p ] = s ;
return s ;
}
2026-11-05 15:22:54 +00:00
public override void Refresh ( )
{
for ( int i = 0 ; i < tabs . Count ; i + + )
{
bool on = i = = page ;
2026-11-09 09:12:53 +00:00
if ( UISkin . On ) UiKit . SetSubTab ( tabs [ i ] , on ) ;
else
{
2026-11-12 13:41:33 +00:00
tabs [ i ] . GetComponent < Image > ( ) . color = on ? UiKit . Red : UiKit . Paper ;
2026-11-09 09:12:53 +00:00
UiKit . LabelOf ( tabs [ i ] ) . color = on ? UiKit . White : UiKit . Ink ;
}
2026-11-05 15:22:54 +00:00
pages [ i ] . gameObject . SetActive ( on ) ;
}
2026-11-12 13:41:33 +00:00
var sc = EnsureScene ( page ) ;
for ( int i = 0 ; i < scenes . Length ; i + + )
if ( scenes [ i ] ! = null & & scenes [ i ] . art ! = null )
scenes [ i ] . art . parent . gameObject . SetActive ( i = = page ) ;
2026-11-05 15:22:54 +00:00
UiKit . Clear ( pages [ page ] ) ;
2026-11-12 13:41:33 +00:00
if ( page = = 0 ) BuildTraining ( pages [ 0 ] , sc ) ;
else if ( page = = 1 ) BuildPatrol ( pages [ 1 ] , sc ) ;
2026-11-05 15:22:54 +00:00
else BuildDev ( pages [ 2 ] ) ;
}
2026-11-12 13:41:33 +00:00
/// <summary>배경 위 반투명 판(배경이 비침 · 테마 dom_panel).</summary>
static RectTransform Glass ( Transform p , string name , float x , float y , float w , float h )
{
var rt = UiKit . At ( p , name , x , y , w , h ) ;
UiKit . Paint ( rt , UiKit . Col ( "dom_panel" , new Color ( 0.12f , 0.14f , 0.21f , 0.78f ) ) ) ;
return rt ;
}
static Text Stroke ( Text t )
{
t . horizontalOverflow = HorizontalWrapMode . Overflow ;
return UiKit . Title ( t , true ) ;
}
2026-11-05 15:22:54 +00:00
// ---------- ① 훈련 ----------
2026-11-12 13:41:33 +00:00
void BuildTraining ( RectTransform p , Scene sc )
2026-11-05 15:22:54 +00:00
{
var db = S . Db ;
var d = S . Save ;
var t = TrainType ;
var types = TrainTypes ;
for ( int i = 0 ; i < types . Count ; i + + )
{
var ut = types [ i ] ;
bool on = ut = = t ;
2026-11-12 13:41:33 +00:00
if ( UISkin . On )
{
var tb = UiKit . SubTab ( p , "Type" + i , UnitTypeInfo . Korean ( ut ) , 24f + i * 208f , 6f , 196f , 78f , ( ) = > SetTrainType ( ut ) ) ;
UiKit . SetSubTab ( tb , on ) ;
// 병종 아이콘(PD 2026-11-12 「보병 = 검 · 창병 = 창 · 기병 = 말 · 궁병 = 활 · 책사 = 깃털 부채」 · 테마 dom_tab_<병종 키>) — 아이콘 왼쪽 · 글자 오른쪽
var ti = UiKit . Icon ( tb . transform , "dom_tab_" + UnitTypeInfo . Key ( ut ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( - 32f , 3f ) , 62f ) ;
if ( ti ! = null )
{
ti . raycastTarget = false ;
UiKit . DropShadow ( ti , 2f ) ;
var tl = UiKit . LabelOf ( tb ) . rectTransform ;
tl . offsetMin = new Vector2 ( tl . offsetMin . x + 56f , tl . offsetMin . y ) ;
}
}
else UiKit . Btn ( "Type" + i , p , UnitTypeInfo . Korean ( ut ) , 36 , on ? UiKit . Red : UiKit . Paper , on ? UiKit . White : UiKit . Ink , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f + i * 208f , - 6f ) , new Vector2 ( 196f , 78f ) , ( ) = > SetTrainType ( ut ) ) ;
2026-11-05 15:22:54 +00:00
}
int rank = TrainingRules . Rank ( d , t ) ;
2026-11-12 13:41:33 +00:00
// 랭크 칩(장면 위) + 병종 보너스 한 줄
var chip = UiKit . Rect ( "Rank" , p , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 100f ) , new Vector2 ( 620f , 68f ) ) ;
UiKit . Skin ( chip , "chip" , UiKit . Col ( "chip_dark" , UiKit . Dark ) ) ;
Stroke ( UiKit . FillLabel ( chip , TrainingRules . RankName ( db , rank ) + " <size=30>랭크 " + rank + "/" + db . Training . MaxRank + "</size>" , 40 , UiKit . White , TextAnchor . MiddleCenter , true , 0f ) ) ;
Stroke ( UiKit . Label ( p , UnitTypeInfo . Korean ( t ) + " 영웅·병사 전원 HP +" + TrainingRules . Bonus ( db , d , t , "hp" ) . ToString ( "0.#" ) + "% · 공 +" + TrainingRules . Bonus ( db , d , t , "atk" ) . ToString ( "0.#" ) + "% · 방 +" +
TrainingRules . Bonus ( db , d , t , "def" ) . ToString ( "0.#" ) + "%" , 26 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 172f ) , new Vector2 ( 1000f , 40f ) , false ) ) ;
// 지휘관 파견 칸(오른쪽 위 · PD 2026-11-12 「훈련에 사용할 영웅을 지정(파견) · 병종 별 지휘관 1명」)
DispatchSlot ( p , DispatchRules . TrainSlot ( t ) , 1056f - 140f , 92f , "지휘관" ) ;
// 연병장 병사 + 지휘관(배경 장면 · 고른 병종) — 배경이 없으면 예전 병종 원
if ( sc ! = null & & sc . amb ! = null ) PlaceSoldiers ( sc , t ) ;
else
{
SoldierCount = 0 ;
var face = UiKit . Dot ( "Face" , p , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - 460f ) , 220f , UiKit . TypeColor ( t ) ) ;
UiKit . FillLabel ( face . transform , UnitTypeInfo . Mark ( t ) , 110 , UiKit . White ) ;
}
// 랭크 진행(반투명 판): 현재 랭크 마커 + 랭크 보너스 · 다음 랭크 승급 비용 · 남은 레벨
// 아래 UI = 페이지 아래끝 쪽으로(PD 2026-11-12 「하단 UI를 좀 더 아래쪽으로」 · 랭크 판 872 · 카드 1016 · 줄 1330)
const float BarY = 872f , CardY = 1016f , FootY = 1330f ;
var bar = Glass ( p , "RankBar" , 24f , BarY , 1032f , 132f ) ;
Stroke ( UiKit . Label ( bar , "랭크 진행" , 28 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f , - 8f ) , new Vector2 ( 300f , 44f ) ) ) ;
2026-11-05 15:22:54 +00:00
int left = TrainingRules . Remaining ( db , d , t ) ;
int total = db . Training . MaxLevel * TrainingTable . Stats . Length ;
2026-11-12 13:41:33 +00:00
var fill = UiKit . Bar ( "Progress" , bar , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 24f , 0f ) , new Vector2 ( 700f , 26f ) , new Color ( 0.32f , 0.28f , 0.35f ) , UiKit . Coin ) ;
2026-11-05 15:22:54 +00:00
float v = total > 0 ? ( total - left ) / ( float ) total : 0f ;
UiKit . SetBar ( fill , v ) ;
2026-11-12 13:41:33 +00:00
var marker = UiKit . Dot ( "Marker" , bar , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 24f + 700f * v , 0f ) , 54f , UiKit . Coin ) ;
UiKit . FillLabel ( marker . transform , "▲" , 26 , UiKit . Dark ) ;
2026-11-05 15:22:54 +00:00
var r = db . Training . RankOf ( rank ) ;
2026-11-12 13:41:33 +00:00
Stroke ( UiKit . Label ( bar , TrainingRules . RankName ( db , rank ) + " · 랭크 보너스 +" + ( r ! = null ? r . rankPct : 0f ) . ToString ( "0.#" ) + "%" , 28 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 24f , 6f ) , new Vector2 ( 700f , 44f ) ) ) ;
string promo = rank > = db . Training . MaxRank ? "최종 랭크" : "다음 랭크 승급 <color=#FFD36A>골드 " + TrainingRules . PromoteCost ( db , d , t ) . ToString ( "N0" ) + "</color>" ;
Stroke ( UiKit . Label ( bar , promo , 24 , UiKit . White , TextAnchor . MiddleRight , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( - 24f , - 8f ) , new Vector2 ( 440f , 40f ) , false ) ) ;
Stroke ( UiKit . Label ( bar , left + " <size=22>남은 레벨</size>" , 36 , UiKit . White , TextAnchor . MiddleRight , new Vector2 ( 1f , 0f ) , new Vector2 ( 1f , 0f ) , new Vector2 ( - 24f , 6f ) , new Vector2 ( 260f , 50f ) ) ) ;
2026-11-05 15:22:54 +00:00
// 카드 3: HP · 공 · 방
string [ ] names = { "HP" , "공격력" , "방어력" } ;
string [ ] marks = { "H" , "공" , "방" } ;
float cw = 330f ;
for ( int i = 0 ; i < 3 ; i + + )
{
int k = i ;
int lv = TrainingRules . Lv ( d , t , i ) ;
bool max = lv > = db . Training . MaxLevel ;
2026-11-12 13:41:33 +00:00
var card = UiKit . Box ( "Card" + i , p , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f + i * ( cw + 21f ) , - CardY ) , new Vector2 ( cw , 300f ) , max ? UiKit . Purple : UiKit . Paper ) ;
2026-11-05 15:22:54 +00:00
UiKit . Border ( card , UiKit . Line , 3f ) ;
2026-11-12 13:41:33 +00:00
var head = UiKit . TopBand ( "Head" , card . transform , 0f , 60f ) ;
2026-11-09 03:20:32 +00:00
UiKit . Paint ( head , max ? UiKit . Purple : UiKit . Dark ) ;
2026-11-12 13:41:33 +00:00
UiKit . FillLabel ( head , max ? "Max" : "LV." + lv , 32 , max ? UiKit . White : UiKit . Ink ) ;
// 항목 아이콘 = 하트 · 검 · 갑옷(PD 2026-11-12 「H = 하트 · 공 = 검 · 방 = 갑옷」 · 테마 dom_stat_*) · 없으면 예전 글자 원
// 받침 = 외곽선 없는 둥근 판 · 세 카드 같은 단색(PD 2026-11-12 「둥근 원형 아웃라인을 쓰지 말고 동일한 색상으로 채워진 백판」 · 테마 dom_icon_plate)
var dot = UiKit . Rect ( "Icon" , card . transform , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 66f ) , new Vector2 ( 90f , 76f ) ) ;
UiKit . Skin ( dot , "soft" , UiKit . Col ( "dom_icon_plate" , UiKit . Dark ) ) ;
var sic = UiKit . Icon ( dot , StatIcons [ i ] , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , 62f ) ; // 받침 안 여백 ≥ 7 px(QA r6 M)
if ( sic ! = null ) UiKit . DropShadow ( sic , 2f ) ;
else UiKit . FillLabel ( dot , marks [ i ] , 36 , UiKit . Ink ) ;
// 이름 + 지금 레벨 수치 한 줄(PD 2026-11-12 「레벨 정보가 중복 → 현재 레벨의 수치만」 · 레벨은 위 띠 LV.n 하나)
var val = UiKit . Label ( card . transform , names [ i ] + " <color=#FFD36A>+" + TrainingRules . Bonus ( db , d , t , TrainingTable . Stats [ i ] ) . ToString ( "0.#" ) + "%</color>" , 36 , UiKit . Ink , TextAnchor . MiddleCenter ,
new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 168f ) , new Vector2 ( 316f , 52f ) ) ;
val . horizontalOverflow = HorizontalWrapMode . Overflow ;
if ( max ) UiKit . Label ( card . transform , "<color=#3a8a30>✔</color>" , 44 , UiKit . Ok , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 14f ) , new Vector2 ( 300f , 62f ) ) ;
2026-11-05 15:22:54 +00:00
else
{
2026-11-12 13:41:33 +00:00
var b = UiKit . Btn ( "Up" , card . transform , TrainingRules . Cost ( db , d , t , i ) . ToString ( "N0" ) + " 골드" , 28 , UiKit . Coin , UiKit . Ink , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 12f ) , new Vector2 ( 280f , 62f ) , ( ) = >
2026-11-05 15:22:54 +00:00
{
var why = S . Do ( ( ) = > TrainingRules . Level ( S . Db , S . Save , TrainType , k ) ) ;
if ( why ! = null ) Root . Toast ( why ) ;
} ) ;
b . interactable = TrainingRules . CanLevel ( db , d , t , i ) = = null ;
}
}
if ( left = = 0 & & rank < db . Training . MaxRank )
{
2026-11-12 13:41:33 +00:00
var pb = UiKit . Btn ( "Promote" , p , "랭크 승급 → " + TrainingRules . RankName ( db , rank + 1 ) + " · 골드 " + TrainingRules . PromoteCost ( db , d , t ) . ToString ( "N0" ) , 34 , UiKit . Red , UiKit . White , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - FootY + 8f ) , new Vector2 ( 900f , 84f ) , ( ) = >
2026-11-05 15:22:54 +00:00
{
var why = S . Do ( ( ) = > TrainingRules . Promote ( S . Db , S . Save , TrainType ) ) ;
Root . Toast ( why ? ? UnitTypeInfo . Korean ( TrainType ) + " 랭크 승급 → " + TrainingRules . RankName ( S . Db , TrainingRules . Rank ( S . Save , TrainType ) ) ) ;
} ) ;
pb . interactable = TrainingRules . CanPromote ( db , d , t ) = = null ;
}
2026-11-12 13:41:33 +00:00
else
Stroke ( UiKit . Label ( p , "3항목 Max → 랭크 승급 · 레벨 리셋 · 랭크 보너스 유지" , 24 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - FootY - 20f ) , new Vector2 ( 1030f , 40f ) , false ) ) ;
2026-11-05 15:22:54 +00:00
}
2026-11-12 13:41:33 +00:00
/// <summary>훈련 카드 항목 아이콘(HP · 공격 · 방어 = 테마 아이콘 id).</summary>
static readonly string [ ] StatIcons = { "dom_stat_hp" , "dom_stat_atk" , "dom_stat_def" } ;
2026-11-05 15:22:54 +00:00
2026-11-12 13:41:33 +00:00
/// <summary>병종의 범용 병사(heroes.csv is_soldier · 첫 줄 = 그 병종 일러).</summary>
HeroDb . Hero SoldierOf ( UnitType t )
2026-11-05 15:22:54 +00:00
{
2026-11-12 13:41:33 +00:00
foreach ( var h in S . Db . Heroes . heroes )
if ( h . isSoldier & & h . type = = t )
return h ;
return null ;
}
2026-11-05 15:22:54 +00:00
2026-11-12 13:41:33 +00:00
/// <summary>
/// 연병장 병사(표 train_slot · 발 = 그림 px) — 그림자·흙먼지(한 아틀라스) 먼저 → 병사(뒷줄부터 · 앞줄이 앞) · 동작 = UiDrillAnim(한 구령 · 뒷줄 f_phase 늦게).
/// 병사는 장면 층(Frame · LobbyAmbient.Lineup)에 둔다 — 매 프레임 그림이 바뀜(P41).
/// </summary>
void PlaceSoldiers ( Scene sc , UnitType t )
{
var line = sc . amb . Lineup ;
UiKit . Clear ( line ) ;
SoldierCount = 0 ;
CommanderShown = false ;
var h = SoldierOf ( t ) ;
var drill = DomesticScene . First ( sc . rows , "drill" ) ; // 공통 줄(맨 앞 · 표준 키 f_h)
var dustRow = DomesticScene . First ( sc . rows , "dust" ) ;
var slots = DomesticScene . All ( sc . rows , "slot" ) ;
if ( h = = null | | drill = = null | | slots . Count = = 0 ) return ;
slots . Sort ( ( a , b ) = > a . y . CompareTo ( b . y ) ) ; // 뒷줄(위) 먼저
float stdPx = drill . h > 0f ? drill . h : 190f ;
// 지휘관(파견 영웅 · 병사 뒷줄 위 · PD 2026-11-12) — 그림자는 병사와 한 번에
var cmdRow = DomesticScene . First ( sc . rows , "cmd" ) ;
var cmd = S . Def ( DispatchRules . Get ( S . Save , DispatchRules . TrainSlot ( t ) ) ) ;
var feet = new List < LobbyScene . Row > ( slots ) ;
if ( cmd ! = null & & cmdRow ! = null ) feet . Add ( cmdRow ) ;
Shadows ( sc , line , feet ) ;
if ( cmd ! = null & & cmdRow ! = null )
CommanderShown = Stand ( sc , line , "Commander" , cmd , cmdRow , stdPx ) ! = null ;
var dusts = new Image [ slots . Count ] ;
if ( dustRow ! = null )
2026-11-05 15:22:54 +00:00
{
2026-11-12 13:41:33 +00:00
var dsp = UISkin . Icon ( dustRow . sprite ) ;
for ( int i = 0 ; i < slots . Count ; i + + )
{
var r = slots [ i ] ;
float k = r . scale > 0.01f ? r . scale : 1f ;
var s = UiKit . Rect ( "Dust" + i , line , DomesticScene . Anchor ( r . x , r . y , sc . artW , sc . artH ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( dustRow . x * k , - dustRow . y * k ) , new Vector2 ( dustRow . w * k , dustRow . h * k ) ) ;
dusts [ i ] = s . gameObject . AddComponent < Image > ( ) ;
dusts [ i ] . sprite = dsp ! = null ? dsp : UiKit . Circle ;
dusts [ i ] . color = dustRow . color ;
dusts [ i ] . raycastTarget = false ;
}
}
var data = UnitArt . Load ( h . art ) ;
LobbyScene . Row footFix = null ; // 그 병종 그림의 뒷발 맞춤(표 train_feet)
foreach ( var fr in DomesticScene . All ( sc . rows , "feet" ) )
if ( fr . sprite = = h . art ) footFix = fr ;
// 병종 그림별 박자(표 train_drill 중 s_sprite = 그 그림 · 없으면 공통 줄) — PD 2026-11-12 기병 「말을 세운 상태에서 돌진 그림은 빠르게 재생되며 원래 상태로」
foreach ( var dr in DomesticScene . All ( sc . rows , "drill" ) )
if ( dr . sprite = = h . art ) drill = dr ;
// 타격 때 날아가는 화살(궁병 · 표 train_shot s_sprite = 「아트 id|화살 아이콘」)
LobbyScene . Row shot = null ;
Sprite shotSp = null ;
foreach ( var sr in DomesticScene . All ( sc . rows , "shot" ) )
2026-11-05 15:22:54 +00:00
{
2026-11-12 13:41:33 +00:00
var ss = sr . Sprites ;
if ( ss . Length > = 2 & & ss [ 0 ] = = h . art )
{
shot = sr ;
shotSp = UISkin . Icon ( ss [ 1 ] ) ;
}
}
for ( int i = 0 ; i < slots . Count ; i + + )
{
var r = slots [ i ] ;
float k = r . scale > 0.01f ? r . scale : 1f ;
var stand = UiKit . UnitStand ( "Soldier" + i , line , h , DomesticScene . Anchor ( r . x , r . y , sc . artW , sc . artH ) , Vector2 . zero , stdPx * k , MountedTop ) ;
if ( stand = = null ) continue ;
var anim = UiDrillAnim . Attach ( stand . GetComponent < Image > ( ) , data , drill , r . phase , k , sc . artW , dusts [ i ] , dustRow , footFix ) ;
if ( anim ! = null & & shot ! = null ) anim . AttachShot ( shotSp , shot ) ;
SoldierCount + + ;
2026-11-05 15:22:54 +00:00
}
2026-11-12 13:41:33 +00:00
}
2026-11-05 15:22:54 +00:00
2026-11-12 13:41:33 +00:00
/// <summary>훈련 장면에 지휘관이 섰는지(테스트).</summary>
public bool CommanderShown { get ; private set ; }
float MountedTop
{
get { return Root . Battle ! = null & & Root . Battle . Config ! = null ? Root . Battle . Config . mountedArtMaxTop : 0f ; }
}
/// <summary>발 자리마다 그림자(표 shadow 줄 · 한 아틀라스 = 한 묶음이라 사람보다 먼저 모두).</summary>
static void Shadows ( Scene sc , RectTransform line , List < LobbyScene . Row > feet )
{
foreach ( var sh in DomesticScene . All ( sc . rows , "shadow" ) )
2026-11-05 15:22:54 +00:00
{
2026-11-12 13:41:33 +00:00
var glow = UISkin . Icon ( sh . sprite ) ;
for ( int i = 0 ; i < feet . Count ; i + + )
2026-11-09 03:24:04 +00:00
{
2026-11-12 13:41:33 +00:00
var r = feet [ i ] ;
float k = r . scale > 0.01f ? r . scale : 1f ;
var s = UiKit . Rect ( "Shadow" + i , line , DomesticScene . Anchor ( r . x , r . y , sc . artW , sc . artH ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( sh . x * k , - sh . y * k ) , new Vector2 ( sh . w * k , sh . h * k ) ) ;
var simg = s . gameObject . AddComponent < Image > ( ) ;
simg . sprite = glow ! = null ? glow : UiKit . Circle ;
simg . color = sh . color ;
simg . raycastTarget = false ;
2026-11-09 03:24:04 +00:00
}
2026-11-05 15:22:54 +00:00
}
2026-11-12 13:41:33 +00:00
}
2026-11-05 15:22:54 +00:00
2026-11-12 13:41:33 +00:00
/// <summary>장면에 영웅·병사 1명(발 = 표 줄 · 키 = 표준 × f_scale · 대기 숨 고르기 클립).</summary>
RectTransform Stand ( Scene sc , RectTransform line , string name , HeroDb . Hero h , LobbyScene . Row r , float stdPx )
{
float k = r . scale > 0.01f ? r . scale : 1f ;
var stand = UiKit . UnitStand ( name , line , h , DomesticScene . Anchor ( r . x , r . y , sc . artW , sc . artH ) , Vector2 . zero , stdPx * k , MountedTop ) ;
if ( stand ! = null ) UiFrameAnim . Attach ( stand . GetComponent < Image > ( ) , UnitArt . Load ( h . art ) , "front_idle" ) ;
return stand ;
2026-11-05 15:22:54 +00:00
}
2026-11-12 13:41:33 +00:00
// ---------- ② 순찰 ----------
2026-11-09 09:12:53 +00:00
/// <summary>
2026-11-12 13:41:33 +00:00
/// 순찰(PD 2026-11-12) — 저잣거리 배경이 잘 보이게: 위 = 순찰 상태(게이지) + 「시간당 수확 ▼」 드롭다운(누르면 펼침) · 가운데 = 배경 · 아래 = 쌓인 보상 + 「수확」 「즉시 수확」.
/// 광고 2배는 수확을 누른 뒤 고르는 팝업(OpenHarvest).
2026-11-09 09:12:53 +00:00
/// </summary>
2026-11-12 13:41:33 +00:00
void BuildPatrol ( RectTransform p , Scene sc )
2026-11-09 09:12:53 +00:00
{
var db = S . Db ;
var d = S . Save ;
var e = db . Economy ;
var row = PatrolRules . Row ( db , d ) ;
float cap = PatrolRules . CapHours ( db , d ) ;
float h = PatrolRules . Hours ( db , d , S . Now ) ;
2026-11-12 13:41:33 +00:00
// 문 앞 순찰 영웅 + 창병 2 + 상인·주민(장면) · 영웅 파견 칸(오른쪽 위) — PD 2026-11-12
if ( sc ! = null & & sc . amb ! = null ) PlacePatrol ( sc ) ;
DispatchSlot ( p , DispatchRules . Patrol , 1056f - 140f , 6f , "순찰 영웅" ) ;
// 배치(PD 「저잣거리가 잘 보이게 · 남은 공간 활용」) = UI 를 모두 아래 한 판에 모음 · 위·가운데는 패루·상점·길
// PD 2026-11-12 예시(정보 판) = 왼쪽 「순찰 · 시계 + 7시간 / 12시간 · 게이지 · 기준」 | 오른쪽 시간당 수확 3줄(늘 보임 · 드롭다운 없앰 · 아이템 = 「전리품」 하나로) / 아래 = 프레임 안 「획득 보상」 + 칸 4
// 판 아래끝 1228 = 즉시 수확 「N시간 분량」 배지(≈1247) 와 띄움
const float W = 1032f , PanelH = 384f , PanelY = 1228f - PanelH , LeftW = 560f , GaugeX = 12f , GaugeY = 42f , FrameY = 176f , FrameH = 198f ;
const float GaugeBar = LeftW - GaugeX - 86f - 12f ; // 게이지 틀 = 배지 86 + 막대 · 왼쪽 칸 안
var a = Glass ( p , "Patrol" , 24f , PanelY , W , PanelH ) ;
// ① 왼쪽 = 순찰 상태 — 배지(쌓인 시간) · 「순찰」 · 시계 + 「7시간 / 12시간」 · 게이지 · 기준 줄
2026-11-09 09:12:53 +00:00
Text badge ;
2026-11-12 13:41:33 +00:00
var fill = UiKit . Gauge ( a , "Gauge" , GaugeX , GaugeY , GaugeBar , "lv_badge" , "grain" , out badge ) ;
2026-11-09 09:12:53 +00:00
UiKit . SetGauge ( fill , cap > 0f ? h / cap : 0f , GaugeBar ) ;
badge . text = Mathf . FloorToInt ( h ) + "h" ;
badge . fontSize = 30 ;
2026-11-12 13:41:33 +00:00
float barX = GaugeX + 86f ; // 막대 왼쪽 끝 = 제목 시작
Stroke ( UiKit . Label ( a , "순찰" , 34 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( barX + 8f , - 4f ) , new Vector2 ( 160f , 50f ) ) ) ;
var tt = Stroke ( UiKit . Label ( a , FormatHours ( h ) + " / " + FormatHours ( cap ) , 30 , UiKit . White , TextAnchor . MiddleRight , new Vector2 ( 0f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( LeftW - 12f , - 4f ) , new Vector2 ( 300f , 50f ) ) ) ;
// 시간 앞 시계(PD 2026-11-12 「시간 정보 앞에는 시계 아이콘」 · 테마 clock · 없으면 글자만)
var ck = UiKit . Icon ( a , "clock" , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( LeftW - 12f - Mathf . Ceil ( tt . preferredWidth ) - 26f , - 29f ) , 42f ) ;
if ( ck ! = null )
{
ck . name = "Clock" ;
UiKit . DropShadow ( ck , 3f ) ;
}
var basis = UiKit . Label ( a , "기준 = 마지막 클리어 " + ( row ! = null ? row . chapter : 0 ) + "장 · 오프라인 누적" , 22 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 26f , - ( GaugeY + 98f ) ) , new Vector2 ( LeftW - 30f , 34f ) , false ) ;
basis . horizontalOverflow = HorizontalWrapMode . Overflow ;
Stroke ( basis ) ;
// ② 오른쪽 = 시간당 수확(간략 · 3줄) — 용어 = 골드 · 경험치 · 전리품(장비·상자 구분 없이 합) · 값 = 「획득량 / 시간」만 오른쪽 정렬(PD 2026-11-12)
var div = UiKit . At ( a , "Divider" , LeftW + 6f , 16f , 2f , FrameY - 30f ) ;
UiKit . Paint ( div , new Color ( 1f , 1f , 1f , 0.18f ) ) ;
float infoW = W - LeftW - 36f ;
var info = UiKit . At ( a , "RateInfo" , LeftW + 22f , 10f , infoW , FrameY - 18f ) ;
string [ ] rIcon = { "cur_gold" , "cur_exp" , "cur_gear" } ;
string [ ] rn = { "골드" , "경험치" , "전리품" } ;
2026-11-09 09:12:53 +00:00
string [ ] rv =
{
PatrolRules . GoldPerHour ( db , d ) . ToString ( "N0" ) ,
PatrolRules . ExpPerHour ( db , d ) . ToString ( "N0" ) ,
2026-11-12 13:41:33 +00:00
( PatrolRules . GearPerHour ( db , d ) + PatrolRules . ChestPerHour ( db , d ) ) . ToString ( "0.##" ) ,
2026-11-09 09:12:53 +00:00
} ;
2026-11-12 13:41:33 +00:00
for ( int i = 0 ; i < rn . Length ; i + + )
2026-11-09 09:12:53 +00:00
{
2026-11-12 13:41:33 +00:00
var r = UiKit . At ( info , "Rate" + i , 0f , i * 52f , infoW , 50f ) ;
var ic = UiKit . Icon ( r , rIcon [ i ] , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 24f , 0f ) , 44f ) ;
2026-11-09 09:12:53 +00:00
if ( ic ! = null ) UiKit . DropShadow ( ic , 3f ) ;
2026-11-12 13:41:33 +00:00
Stroke ( UiKit . Label ( r , rn [ i ] , 27 , UiKit . Col ( "dom_rate_name" , new Color ( 0.91f , 0.86f , 0.77f ) ) , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 56f , 0f ) , new Vector2 ( 150f , 50f ) ) ) ;
var v = Stroke ( UiKit . Label ( r , rv [ i ] + "<size=20> / 시간</size>" , 28 , UiKit . White , TextAnchor . MiddleRight , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 6f , 0f ) , new Vector2 ( infoW - 210f , 50f ) ) ) ;
v . name = "PerHour" ;
2026-11-09 09:12:53 +00:00
}
2026-11-12 13:41:33 +00:00
// ③ 아래 = 획득 보상 — 프레임(테마 dom_reward_frame · 9-slice = 네 모서리 장식 그대로 · 없으면 프레임 없이) 안 왼쪽 글자 + 칸 4
2026-11-09 09:12:53 +00:00
var y = PatrolRules . Preview ( db , d , h , 1f ) ;
2026-11-12 13:41:33 +00:00
var fsp = UISkin . Icon ( "dom_reward_frame" ) ;
if ( fsp ! = null )
{
var fr = UiKit . At ( a , "RewardFrame" , 10f , FrameY , W - 20f , FrameH ) ;
var fimg = fr . gameObject . AddComponent < Image > ( ) ;
fimg . sprite = fsp ;
fimg . raycastTarget = false ;
if ( fsp . border ! = Vector4 . zero )
{
fimg . type = Image . Type . Sliced ;
fimg . pixelsPerUnitMultiplier = 2f ; // 테두리(모서리 매듭·막대)를 그림의 절반 크기로 = 얇은 금테(PD 예시)
}
}
Stroke ( UiKit . Label ( a , "획득\n보상" , 30 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 50f , - ( FrameY + FrameH * 0.5f ) ) , new Vector2 ( 104f , 90f ) ) ) ;
RewardRow ( a , y , db , d , 164f , W - 10f - 36f - 164f , FrameY + 30f , 128f ) ; // 칸 위 30 · 수량 칩 아래 ≈ 8 = 금테 막대 안쪽(QA r9 M⑥ 칩이 막대에 붙음)
2026-11-09 09:12:53 +00:00
2026-11-12 13:41:33 +00:00
// ④ 버튼 2개(PD 2026-11-12 「순찰 메뉴에는 수확과 즉시 수확만」)
var hb = UiKit . SkinBtn ( p , "Harvest" , h > 0f ? "btn_l_green" : "btn_gray" , UiKit . White , "수확" , 52 , UiKit . Col ( "text_on_green" , UiKit . White ) , 24f , 1270f , 505f , 128f , OpenHarvest ) ;
2026-11-09 09:12:53 +00:00
hb . interactable = h > 0f ;
2026-11-12 13:41:33 +00:00
var ins = UiKit . SkinBtn ( p , "Instant" , "btn_purple" , UiKit . White , "즉시 수확" , 44 , UiKit . White , 1056f - 505f , 1270f , 505f , 128f , ( ) = >
2026-11-09 09:12:53 +00:00
{
string why ;
var r = S . InstantHarvest ( out why ) ;
if ( r = = null ) Root . Toast ( why ) ;
else ShowYield ( "즉시 수확" , r ) ;
} ) ;
UiKit . BtnCost ( ins , "cur_gem" , e . PatrolInstantGem . ToString ( ) , 40f , 32 ) ;
2026-11-12 13:41:33 +00:00
// 즉시 수확 분량 = 버튼 위끝에 걸친 배지(칩)
var hc = UiKit . Skin ( UiKit . Rect ( "Hours" , ins . transform , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - 2f ) , new Vector2 ( 200f , 42f ) ) , "chip" , UiKit . Col ( "chip_dark" , UiKit . Dark ) ) ;
Stroke ( UiKit . FillLabel ( hc . transform , FormatHours ( e . PatrolInstantHours ) + " 분량" , 24 , UiKit . White , TextAnchor . MiddleCenter , true , 0f ) ) ;
}
/// <summary>순찰 장면에 선 NPC 수 · 창병 수 · 영웅 여부(테스트).</summary>
public int NpcCount { get ; private set ; }
public int GuardCount { get ; private set ; }
public bool PatrolHeroShown { get ; private set ; }
/// <summary>
/// 저잣거리 장면(PD 2026-11-12 「순찰 영웅은 문 앞에서 창병 2기를 양쪽에 두고 대기 자세」 · 「상인 및 주민 NPC 등이 보이게」) —
/// 표 patrol_hero · patrol_guard · patrol_npc 줄(그림 px) · 그림자 먼저 → 위(뒤)에서 아래(앞) 순서. NPC 는 움직이지 않는 그림.
/// </summary>
void PlacePatrol ( Scene sc )
{
var line = sc . amb . Lineup ;
UiKit . Clear ( line ) ;
NpcCount = GuardCount = 0 ;
PatrolHeroShown = false ;
var unit = DomesticScene . First ( sc . rows , "unit" ) ;
float stdPx = unit ! = null & & unit . h > 0f ? unit . h : 180f ;
var heroRow = DomesticScene . First ( sc . rows , "hero" ) ;
var hero = S . Def ( DispatchRules . Get ( S . Save , DispatchRules . Patrol ) ) ;
var guards = DomesticScene . All ( sc . rows , "guard" ) ;
var npcs = DomesticScene . All ( sc . rows , "npc" ) ;
var people = new List < LobbyScene . Row > ( guards ) ;
if ( hero ! = null & & heroRow ! = null ) people . Add ( heroRow ) ;
people . AddRange ( npcs ) ;
Shadows ( sc , line , people ) ;
people . Sort ( ( a , b ) = > a . y . CompareTo ( b . y ) ) ; // 위(뒤) 먼저
foreach ( var r in people )
{
if ( r . kind = = "npc" )
{
// 상인·주민 = 유닛 아트(Resources/Units/npc_*) 대기 숨 고르기(유닛과 같은 R22 클립 · PD 2026-11-12 「NPC 들이 유닛 대기 모션처럼」) · 키 = 표 f_h(그림 몸 높이 기준)
var data = UnitArt . Load ( r . sprite ) ;
Sprite [ ] fr ;
if ( data = = null | | data . Meta . pivot = = null | | data . Meta . pivot . Length < 2 | | ! data . Clips . TryGetValue ( "front_idle" , out fr ) | | fr . Length = = 0 ) continue ;
float hh = r . h > 0f ? r . h : 150f ;
float k = hh / Mathf . Max ( 1f , data . BodyPx ) ;
var rt = UiKit . Rect ( "Npc_" + r . sprite , line , DomesticScene . Anchor ( r . x , r . y , sc . artW , sc . artH ) , new Vector2 ( 0.5f , data . Meta . pivot [ 1 ] ) , Vector2 . zero , fr [ 0 ] . rect . size * k ) ;
var img = rt . gameObject . AddComponent < Image > ( ) ;
img . sprite = fr [ 0 ] ;
img . raycastTarget = false ;
if ( r . n = = 1 ) rt . localScale = new Vector3 ( - 1f , 1f , 1f ) ;
UiFrameAnim . Attach ( img , data , "front_idle" ) ;
NpcCount + + ;
}
else if ( r . kind = = "guard" )
{
var g = S . Def ( r . sprite ) ;
if ( g ! = null & & Stand ( sc , line , "Guard" + r . slot , g , r , stdPx ) ! = null ) GuardCount + + ;
}
else if ( hero ! = null & & Stand ( sc , line , "PatrolHero" , hero , r , stdPx ) ! = null ) PatrolHeroShown = true ;
}
}
/// <summary>쌓인 보상 타일 4(골드 · 병사 경험 · 승급 장비 · 청동 상자) — x ~ x+w 안 가운데 정렬.</summary>
static void RewardRow ( Transform c , PatrolYield y , OutGameData db , SaveData d , float x , float w , float top , float tile )
{
string [ ] rIcon = { "cur_gold" , "cur_exp" , "cur_gear" , "chest_bronze" } ;
string [ ] sn = { y . gold . ToString ( "N0" ) , y . exp . ToString ( "N0" ) , y . gear . ToString ( ) , y . chests . ToString ( ) } ;
float gap = Mathf . Min ( 72f , ( w - 4f * tile - 140f ) / 3f ) ;
float x0 = x + ( w - 4f * tile - 3f * gap ) * 0.5f ;
for ( int i = 0 ; i < 4 ; i + + )
UiKit . RewardTile ( c , "Tile" + i , x0 + i * ( tile + gap ) , top , tile , i = = 2 ? "gear_" + PatrolRules . GearGrade ( db , d ) + "_" : "gear_white_" , rIcon [ i ] , sn [ i ] , i = = 2 ) ;
}
/// <summary>수확 팝업(PD 2026-11-12 「광고 2배 보상은 수확 버튼을 눌렀을 때 선택」) — 쌓인 보상 + 「수확」 · 「광고 2배」(하루 n/m회).</summary>
public void OpenHarvest ( )
{
var db = S . Db ;
var d = S . Save ;
var e = db . Economy ;
float h = PatrolRules . Hours ( db , d , S . Now ) ;
if ( h < = 0f )
{
Root . Toast ( "쌓인 보상이 없습니다" ) ;
return ;
}
EconomyRules . RollDaily ( d , S . Now ) ;
var content = Root . ModalCustom ( "수확" , 700f ) ;
var panel = content . parent ;
UiKit . Label ( content , FormatHours ( h ) + " 분량" , 36 , UiKit . Ink , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 4f ) , new Vector2 ( 800f , 54f ) ) ;
RewardRow ( content , PatrolRules . Preview ( db , d , h , 1f ) , db , d , 0f , 860f , 76f , 132f ) ;
bool adOk = d . adsUsed . patrol < e . PatrolAdDaily ;
UiKit . Label ( content , "광고를 보면 보상 2배 · 오늘 " + d . adsUsed . patrol + "/" + e . PatrolAdDaily + "회" , 28 , UiKit . Col ( "text_soft" , UiKit . InkSoft ) , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 250f ) , new Vector2 ( 800f , 44f ) , false ) ;
var hb = UiKit . SkinBtn ( panel , "Take" , "btn_l_green" , UiKit . White , "수확" , 44 , UiKit . Col ( "text_on_green" , UiKit . White ) , 40f , 700f - 150f , 420f , 116f , ( ) = >
{
Root . CloseModal ( ) ;
DoHarvest ( false ) ;
} ) ;
var ad = UiKit . SkinBtn ( panel , "Ad" , adOk ? "btn_sky" : "btn_gray" , UiKit . White , "광고 2배" , 40 , UiKit . White , 940f - 40f - 420f , 700f - 150f , 420f , 116f , ( ) = >
{
Root . CloseModal ( ) ;
DoHarvest ( true ) ;
} ) ;
ad . interactable = adOk ;
UiKit . BtnCost ( ad , "ad" , d . adsUsed . patrol + "/" + e . PatrolAdDaily , 34f , 28 ) ;
hb . name = "Take" ;
2026-11-09 09:12:53 +00:00
}
2026-11-05 15:22:54 +00:00
void DoHarvest ( bool ad )
{
string why ;
var r = S . Harvest ( ad , out why ) ;
if ( r = = null ) Root . Toast ( why ) ;
else ShowYield ( ad ? "광고 2배 수확" : "수확" , r ) ;
}
void ShowYield ( string title , PatrolYield y )
{
var sb = new System . Text . StringBuilder ( ) ;
sb . Append ( FormatHours ( y . hours ) ) . Append ( y . mult > 1f ? " × " + y . mult : "" ) . Append ( " 분량\n\n" ) ;
2026-11-12 13:41:33 +00:00
// 용어 = 골드 · 경험치 · 전리품(PD 2026-11-12 「3가지로 용어를 통일」) · 전리품 = 받은 장비·상자 이름
sb . Append ( "골드 +" ) . Append ( y . gold . ToString ( "N0" ) ) . Append ( "\n경험치 +" ) . Append ( y . exp ) . Append ( " (" ) . Append ( y . expTo . Count ) . Append ( "명)" ) ;
var loot = new List < string > ( ) ;
foreach ( var g in y . gearIds ) loot . Add ( S . Db . Gear . Get ( g ) . nameKo ) ;
if ( y . chestIds . Count > 0 ) loot . Add ( S . Db . Chests . Get ( y . chestIds [ 0 ] ) . nameKo + " × " + y . chestIds . Count ) ;
if ( loot . Count > 0 ) sb . Append ( "\n전리품 " ) . Append ( string . Join ( " · " , loot . ToArray ( ) ) ) ;
2026-11-05 15:22:54 +00:00
Root . Modal ( title , sb . ToString ( ) , new OutGameRoot . ModalButton ( "확인" , UiKit . Dark , null ) ) ;
}
public static string FormatHours ( float h )
{
int min = Mathf . FloorToInt ( h * 60f + 1e-3f ) ;
return ( min / 60 ) + "시간" + ( min % 60 > 0 ? " " + ( min % 60 ) + "분" : "" ) ;
}
2026-11-12 13:41:33 +00:00
// ---------- 영웅 파견(훈련 지휘관 · 순찰 · 개발 · PD 2026-11-12) ----------
/// <summary>파견 칸(왼쪽 위 기준 140× 150) — 파견됨 = 영웅 초상 원 + 이름표 · 비어 있음 = 「+」 · 누르면 고르기 창.</summary>
void DispatchSlot ( Transform p , string slot , float x , float y , string caption )
2026-11-05 15:22:54 +00:00
{
2026-11-12 13:41:33 +00:00
var box = UiKit . At ( p , "Dispatch" , x , y , 140f , 150f ) ;
var hit = box . gameObject . AddComponent < Image > ( ) ;
hit . color = new Color ( 1f , 1f , 1f , 0f ) ;
hit . raycastTarget = true ;
box . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ( ) = > OpenDispatch ( slot ) ) ;
var h = S . Def ( DispatchRules . Get ( S . Save , slot ) ) ;
if ( h ! = null ) UiKit . Portrait ( "Face" , box , h , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , Vector2 . zero , new Vector2 ( 116f , 116f ) , true , 2.1f , UiKit . Coin ) ;
else
{
var c = UiKit . Rect ( "Empty" , box , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , Vector2 . zero , new Vector2 ( 116f , 116f ) ) ;
var ci = UiKit . Skin ( c , "circle122" , UiKit . Col ( "menu_btn" , UiKit . Dark ) ) ;
ci . preserveAspect = true ;
Stroke ( UiKit . FillLabel ( c , "+" , 64 , UiKit . White , TextAnchor . MiddleCenter , true , 0f ) ) ;
}
var tag = UiKit . Skin ( UiKit . Rect ( "Tag" , box , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , Vector2 . zero , new Vector2 ( 150f , 40f ) ) , "chip" , UiKit . Col ( "chip_dark" , UiKit . Dark ) ) ;
Stroke ( UiKit . FillLabel ( tag . transform , h ! = null ? h . nameKo : caption , 24 , UiKit . White , TextAnchor . MiddleCenter , true , 0f ) ) ;
2026-11-05 15:22:54 +00:00
}
2026-11-12 13:41:33 +00:00
/// <summary>파견 고르기 창 — 보유 영웅(훈련 = 그 병종만) 초상 격자 · 다른 자리에 있으면 「○○ 중」 표시(고르면 옮김) · 해제 · 닫기.</summary>
public void OpenDispatch ( string slot )
{
var db = S . Db ;
var d = S . Save ;
var list = DispatchRules . Candidates ( db , d , slot ) ;
string cur = DispatchRules . Get ( d , slot ) ;
string title = slot = = DispatchRules . Patrol ? "순찰 영웅 파견" : slot = = DispatchRules . Dev ? "개발 영웅 배치" : DispatchRules . SlotKo ( slot ) + " 지휘관 파견" ;
var content = Root . ModalCustom ( title , 1180f ) ;
var panel = content . parent ;
DispatchChoices = list . Count ;
if ( list . Count = = 0 )
UiKit . FillLabel ( content , slot . StartsWith ( "train:" ) ? "이 병종의 보유 영웅이 없습니다" : "보유 영웅이 없습니다" , 34 , UiKit . Ink , TextAnchor . MiddleCenter , true , 0f ) ;
else
{
ScrollRect sr ;
var grid = UiKit . ScrollGrid ( "Pick" , content , 0f , 0f , 0f , 0f , new Vector2 ( 196f , 236f ) , new Vector2 ( 16f , 16f ) , 4 , out sr ) ;
foreach ( var hh in list )
{
var h = hh ;
var cell = new GameObject ( "Hero_" + h . id , typeof ( RectTransform ) ) . GetComponent < RectTransform > ( ) ;
cell . SetParent ( grid , false ) ;
var ci = cell . gameObject . AddComponent < Image > ( ) ;
ci . color = new Color ( 1f , 1f , 1f , 0f ) ;
ci . raycastTarget = true ;
cell . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ( ) = >
{
var why = S . Do ( ( ) = > DispatchRules . Assign ( S . Db , S . Save , slot , h . id ) ) ;
if ( why ! = null ) Root . Toast ( why ) ;
else Root . CloseModal ( ) ;
} ) ;
UiKit . Portrait ( "Face" , cell , h , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 4f ) , new Vector2 ( 172f , 172f ) , true , 2.1f , h . id = = cur ? UiKit . Coin : UiKit . Line ) ;
Stroke ( UiKit . Label ( cell , h . nameKo , 28 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 14f ) , new Vector2 ( 196f , 40f ) ) ) ;
var at = DispatchRules . Where ( d , h . id ) ;
if ( at ! = null )
{
var chip = UiKit . Skin ( UiKit . Rect ( "Where" , cell , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - 150f ) , new Vector2 ( 170f , 36f ) ) , "chip" , at = = slot ? UiKit . Coin : UiKit . Col ( "chip_dark" , UiKit . Dark ) ) ;
Stroke ( UiKit . FillLabel ( chip . transform , at = = slot ? "배치 중" : DispatchRules . SlotKo ( at ) + " 중" , 22 , UiKit . White , TextAnchor . MiddleCenter , true , 0f ) ) ;
}
}
}
if ( ! string . IsNullOrEmpty ( cur ) )
UiKit . SkinBtn ( panel , "Clear" , "btn_gray" , UiKit . White , "해제" , 38 , UiKit . White , 40f , 1180f - 142f , 420f , 108f , ( ) = >
{
S . Do ( ( ) = > DispatchRules . Clear ( S . Save , slot ) ) ;
Root . CloseModal ( ) ;
} ) ;
UiKit . SkinBtn ( panel , "Close" , "btn_sky" , UiKit . White , "닫기" , 38 , UiKit . White , 940f - 40f - 420f , 1180f - 142f , 420f , 108f , Root . CloseModal ) ;
}
/// <summary>마지막으로 연 고르기 창의 후보 수(테스트).</summary>
public int DispatchChoices { get ; private set ; }
2026-11-05 15:22:54 +00:00
// ---------- ③ 개발 ----------
2026-11-12 13:41:33 +00:00
/// <summary>
/// 개발(PD 2026-11-12 「규장각 같은 연구 장소를 배경 · 양피지나 서적 내용에 성장 트리」 + 같은 날 개편 「재화 정보 판·글자 줄 제거 → 그 자리에 아이콘·재화 · 양피지 좌우 넓힘 · 트리 초기화 = 아래 두루마리」 · 「개발 메뉴에도 영웅 배치」) —
/// 두루마리(dom_scroll · 좌우 9-slice = 금 마개 그대로 가운데만 늘림) 위 막대 = 갈래 이름 ◀ ▶ · 종이 = 쪽 점 · 도면·골드 · 트리(먹 선 = 노드 가운데끼리 · 이름 = 노드 옆) · 아래 막대 = 트리 초기화 · 왼쪽 아래 = 배치 영웅.
/// 종이 사각 = 표 dev_paper(원본 px · 실측) · 그림이 없으면 같은 자리에 종이색 판.
/// </summary>
2026-11-05 15:22:54 +00:00
void BuildDev ( RectTransform p )
{
var db = S . Db ;
var d = S . Save ;
var branches = db . Dev . Branches ;
if ( branches . Count = = 0 ) return ;
int n = branches . Count ;
string br = Branch ;
2026-11-12 13:41:33 +00:00
var ink = UiKit . Col ( "dom_ink" , new Color ( 0.35f , 0.23f , 0.13f ) ) ;
var inkSoft = UiKit . Col ( "dom_ink_soft" , new Color ( 0.54f , 0.42f , 0.30f ) ) ;
// 두루마리(표 dev_scroll · 화면 폭 f_x × 높이 f_y · 좌우 테두리 = 원본 px 그대로)
var sc = scenes [ 2 ] ! = null ? scenes [ 2 ] . rows : DomesticScene . Page ( "dev" ) ;
var scrollRow = DomesticScene . First ( sc , "scroll" ) ;
var paperRow = DomesticScene . First ( sc , "paper" ) ;
var sp = scrollRow ! = null ? UISkin . Icon ( scrollRow . sprite ) : null ;
float SH = scrollRow ! = null & & scrollRow . y > 0f ? scrollRow . y : 1290f ;
float bw = scrollRow ! = null & & scrollRow . w > 0f ? scrollRow . w : ( sp ! = null ? sp . rect . width : 1f ) ;
float bh = scrollRow ! = null & & scrollRow . h > 0f ? scrollRow . h : ( sp ! = null ? sp . rect . height : 1f ) ;
float ry = SH / Mathf . Max ( 1f , bh ) ;
float sw = scrollRow ! = null & & scrollRow . x > 0f ? scrollRow . x : SH * bw / Mathf . Max ( 1f , bh ) ;
var scroll = UiKit . Rect ( "Scroll" , p , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , Vector2 . zero , new Vector2 ( sw , SH ) ) ;
Rect paper ; // 두루마리 안 종이(왼쪽 위 기준 · 아래로 +)
if ( sp ! = null & & paperRow ! = null )
{
var simg = scroll . gameObject . AddComponent < Image > ( ) ;
simg . sprite = sp ;
simg . raycastTarget = false ;
// 좌우 테두리(spriteBorder) 가 있으면 가운데만 늘림 — 종이 왼끝 = 원본 px 그대로 · 오른끝 = 화면 폭 − (원본 폭 − 원본 오른끝)
float bl = 0f , brd = 0f ;
if ( sp . border . x > 0f | | sp . border . z > 0f )
{
simg . type = Image . Type . Sliced ;
float k = sp . rect . height / Mathf . Max ( 1f , bh ) ; // 임포트 px ÷ 원본 px
simg . pixelsPerUnitMultiplier = k / Mathf . Max ( 0.01f , ry ) ; // 테두리 = 원본 px × 세로 배율
bl = sp . border . x / k * ry ;
brd = sp . border . z / k * ry ;
}
float px0 = paperRow . x * ry , px1 = sw - ( bw - ( paperRow . x + paperRow . w ) ) * ry ;
if ( bl < = 0f ) // 테두리 없음 = 비율대로
{
float rx = sw / Mathf . Max ( 1f , bw ) ;
px0 = paperRow . x * rx ;
px1 = ( paperRow . x + paperRow . w ) * rx ;
}
paper = new Rect ( px0 , paperRow . y * ry , px1 - px0 , paperRow . h * ry ) ;
}
else
{
UiKit . Paint ( scroll , new Color ( 0.97f , 0.91f , 0.78f ) ) ;
paper = new Rect ( 40f , 140f , sw - 80f , SH - 200f ) ;
}
// 위 막대 = 갈래 이름 + ◀ ▶ · 아래 막대 = 트리 초기화
float rollY = Mathf . Max ( 60f , paper . y * 0.46f ) ;
Stroke ( UiKit . Label ( scroll , BranchKo ( br ) , 50 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - rollY ) , new Vector2 ( 360f , 70f ) ) ) ;
ArrowBtn ( scroll , "Prev" , "◀" , - 250f , rollY , ( ) = > SetBranch ( branchIdx - 1 ) ) ;
ArrowBtn ( scroll , "Next" , "▶" , 250f , rollY , ( ) = > SetBranch ( branchIdx + 1 ) ) ;
float rollB = paper . yMax + ( SH - paper . yMax ) * 0.5f ; // 아래 막대 가운데
// 아래 막대 = [영웅 배치 칸(다른 탭과 같은 파견 칸 · PD 2026-11-12)] [트리 초기화] — 둘을 막대 가운데에 나란히(왼쪽 아래 영웅 상반신과 안 겹치게 조금 오른쪽)
// PD 2026-11-12 「배치 영웅의 위치를 좌측으로 조금 더 · 초기화 버튼은 중앙 정렬」 = 버튼 = 막대 가운데 · 파견 칸 = 왼쪽(금 마개 안쪽)
const float ResetW = 320f ;
DispatchSlot ( scroll , DispatchRules . Dev , sw * 0.15f , rollB - 75f , "영웅 배치" ) ;
DevHeroShown = S . Def ( DispatchRules . Get ( d , DispatchRules . Dev ) ) ! = null ;
var rb = UiKit . SkinBtn ( scroll , "Reset" , "btn_purple" , UiKit . White , "트리 초기화" , 30 , UiKit . White , ( sw - ResetW ) * 0.5f , rollB - 46f , ResetW , 92f , ( ) = >
{
Root . Modal ( "트리 초기화" , "모든 개발 노드를 0 으로 되돌립니다.\n보옥 " + db . Economy . DevResetGem + " 소모 · 쓴 도면 " + S . Save . dev . blueprintSpent + " 환급 · 골드 환급 없음" ,
new OutGameRoot . ModalButton ( "초기화" , UiKit . Red , ( ) = > Root . Toast ( S . Do ( ( ) = > DevRules . Reset ( S . Db , S . Save ) ) ? ? "개발 트리 초기화" ) ) ,
new OutGameRoot . ModalButton ( "취소" , UiKit . Dark , null ) ) ;
} ) ;
UiKit . BtnCost ( rb , "cur_gem" , db . Economy . DevResetGem . ToString ( ) , 34f , 26 ) ;
// 종이 머리: 쪽 점 + 도면·골드(예전 갈래 설명 글자 줄 자리 · PD 2026-11-12)
var pr = UiKit . At ( scroll , "Paper" , paper . x , paper . y , paper . width , paper . height ) ;
for ( int i = 0 ; i < n ; i + + ) UiKit . Dot ( "Page" + i , pr , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( ( i - ( n - 1 ) / 2f ) * 30f , - 22f ) , 16f , i = = branchIdx ? UiKit . Red : inkSoft ) ;
// 재화 = 양피지 색 둥근 받침(단색 · 짙은 색 금지 · PD 2026-11-12) 2개 · 받침 사이 넓게
var cur = UiKit . Rect ( "Currency" , pr , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 36f ) , new Vector2 ( paper . width , 58f ) ) ;
var hl = cur . gameObject . AddComponent < HorizontalLayoutGroup > ( ) ;
hl . childAlignment = TextAnchor . MiddleCenter ;
hl . spacing = 170f ; // PD 2026-11-12 「재화 간 간격을 더 넓히고」 2회(56 → 120 → 170)
hl . childControlWidth = true ;
hl . childControlHeight = false ;
hl . childForceExpandWidth = hl . childForceExpandHeight = false ;
CurrencyItem ( cur , "cur_blueprint" , "도" , new Color ( 0.33f , 0.45f , 0.62f ) , "도면 × " + d . inv . blueprint , ink ) ;
CurrencyItem ( cur , "cur_gold" , "골" , UiKit . Coin , d . gold . ToString ( "N0" ) , ink ) ;
2026-11-05 15:22:54 +00:00
// 깊이(선행 조건) = 줄 · 같은 줄 = 가로로
var nodes = db . Dev . InBranch ( br ) ;
var depth = new Dictionary < string , int > ( ) ;
int maxDepth = 0 ;
foreach ( var nd in nodes )
{
int dp = 0 ;
foreach ( var r in nd . requires )
{
int x ;
if ( depth . TryGetValue ( r . node , out x ) ) dp = Mathf . Max ( dp , x + 1 ) ;
}
depth [ nd . id ] = dp ;
maxDepth = Mathf . Max ( maxDepth , dp ) ;
}
2026-11-12 13:41:33 +00:00
const float TreeTop = 100f , TreeBottom = 12f ;
var tree = UiKit . Fill ( "Tree" , pr , 0f , 0f , TreeTop , TreeBottom ) ;
float th = paper . height - TreeTop - TreeBottom ;
float rowH = th / ( maxDepth + 1 ) ;
// 이름 글자 없음(PD 2026-11-12 「텍스트는 제거 · 대신 아이콘 크기를 키우고 가로 1열에 아이콘을 놓을 수 있게」 · 이름 = 노드를 누르면 팝업) —
// 한 줄 = 종이 폭에 고르게(2개 = 가운데 ± 종이 28 % · 더 많으면 종이 88 % 안에 나눔)
2026-11-05 15:22:54 +00:00
var pos = new Dictionary < string , Vector2 > ( ) ;
for ( int dp = 0 ; dp < = maxDepth ; dp + + )
{
var row = new List < DevTree . Node > ( ) ;
foreach ( var nd in nodes )
if ( depth [ nd . id ] = = dp ) row . Add ( nd ) ;
2026-11-12 13:41:33 +00:00
float step = row . Count > 1 ? Mathf . Min ( paper . width * 0.56f , paper . width * 0.88f / ( row . Count - 1 ) ) : 0f ;
2026-11-05 15:22:54 +00:00
for ( int i = 0 ; i < row . Count ; i + + )
2026-11-12 13:41:33 +00:00
pos [ row [ i ] . id ] = new Vector2 ( ( i - ( row . Count - 1 ) / 2f ) * step , - rowH * ( dp + 0.5f ) ) ;
2026-11-05 15:22:54 +00:00
}
2026-11-12 13:41:33 +00:00
float big = Mathf . Min ( 132f , rowH * 0.80f ) , small = Mathf . Min ( 112f , rowH * 0.70f ) ;
// 선 = 노드 가운데끼리(끝은 노드 원이 덮음 · 줄 사이에 글자가 없어 끊기지 않음 · QA r2 B2)
// 두 줄 이상 건너는 선 = 꺾은선(위 노드에서 곧장 아래 → 아래 노드 높이에서 옆으로 진입) — 사선이 가운데 줄 노드·알약을 스치지 않게(QA r3 F1)
2026-11-05 15:22:54 +00:00
foreach ( var nd in nodes )
foreach ( var r in nd . requires )
2026-11-12 13:41:33 +00:00
{
if ( ! pos . ContainsKey ( r . node ) ) continue ;
var a = pos [ r . node ] ;
var b = pos [ nd . id ] ;
var col = DevRules . Level ( d , r . node ) > = r . lv ? ink : new Color ( inkSoft . r , inkSoft . g , inkSoft . b , 0.55f ) ;
if ( depth [ nd . id ] - depth [ r . node ] > 1 & & Mathf . Abs ( a . x - b . x ) > 1f )
{
var corner = new Vector2 ( a . x , b . y ) ;
Line ( tree , a , corner + new Vector2 ( 0f , - 3f ) , col ) ;
Line ( tree , corner + new Vector2 ( a . x > b . x ? 3f : - 3f , 0f ) , b , col ) ;
}
else Line ( tree , a , b , col ) ;
}
2026-11-05 15:22:54 +00:00
NodeCount = 0 ;
2026-11-12 13:41:33 +00:00
IconNodeCount = 0 ;
2026-11-05 15:22:54 +00:00
foreach ( var nd in nodes )
{
var node = nd ;
int lv = DevRules . Level ( d , nd . id ) ;
bool open = DevRules . Unlocked ( d , nd ) ;
bool max = lv > = nd . lvMax ;
2026-11-12 13:41:33 +00:00
float size = nd . Big ? big : small ;
var face = max ? UiKit . Coin : open ? new Color ( 1f , 0.97f , 0.88f ) : new Color ( 0.80f , 0.74f , 0.63f ) ;
var c = UiKit . Btn ( "Node_" + nd . id , tree , "" , 30 , face , ink , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , pos [ nd . id ] , new Vector2 ( size , size ) , ( ) = > NodePopup ( node . id ) ) ;
2026-11-05 15:22:54 +00:00
var img = c . GetComponent < Image > ( ) ;
img . sprite = UiKit . Circle ;
img . type = Image . Type . Simple ;
2026-11-12 13:41:33 +00:00
UiKit . Border ( img , nd . Big ? new Color ( 0.78f , 0.55f , 0.18f ) : ink , nd . Big ? 6f : 4f ) ;
var lab = UiKit . LabelOf ( c ) ;
// 노드 그림 아이콘(테마 dev_<node_id> · PD 2026-11-12 「노드마다 그림 아이콘」) · 잠김 = 흐리게 + 자물쇠 · 그림이 없으면 예전 첫 글자
var ic = UiKit . Icon ( c . transform , "dev_" + nd . id , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , 4f ) , size * 0.74f ) ;
if ( ic ! = null )
{
lab . text = "" ;
ic . raycastTarget = false ;
if ( ! open )
{
ic . color = UiKit . Col ( "dom_node_locked" , new Color ( 0.62f , 0.58f , 0.52f , 0.85f ) ) ;
var lk = UiKit . Icon ( c . transform , "lock_s" , new Vector2 ( 1f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( - size * 0.12f , - size * 0.12f ) , size * 0.34f ) ;
if ( lk ! = null ) lk . raycastTarget = false ;
}
IconNodeCount + + ;
}
else
{
lab . text = open ? nd . nameKo . Substring ( 0 , 1 ) : "잠" ;
lab . fontSize = nd . Big ? 56 : 46 ;
lab . color = open ? ink : inkSoft ;
}
var pill = UiKit . Box ( "Lv" , c . transform , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , new Vector2 ( 100f , 32f ) , max ? UiKit . Purple : UiKit . Dark ) ;
Stroke ( UiKit . FillLabel ( pill . transform , lv + "/" + nd . lvMax , 22 , UiKit . White ) ) ;
2026-11-05 15:22:54 +00:00
NodeCount + + ;
}
2026-11-12 13:41:33 +00:00
// 개발 배치 영웅 = 아래 두루마리 파견 칸에만(왼쪽 아래 큰 상반신은 PD 2026-11-12 「거대한 유닛 얼굴은 제거」)
}
/// <summary>재화 받침 1개(테마 chip 스킨 · 색 dom_chip = 양피지보다 조금 짙은 황갈 · 아이콘 + 먹 글자 · 폭 = 내용에 맞춤).</summary>
static void CurrencyItem ( RectTransform row , string icon , string mark , Color dot , string text , Color ink )
{
var pill = UiKit . Rect ( "Pill_" + icon , row , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , new Vector2 ( 10f , 58f ) ) ;
UiKit . Skin ( pill , "soft" , UiKit . Col ( "dom_chip" , new Color ( 0.90f , 0.80f , 0.60f ) ) ) ; // 외곽선 없는 둥근 판(PD 2026-11-12 「백판 아웃라인 제거」 · 예전 chip = 테두리 있음)
var hl = pill . gameObject . AddComponent < HorizontalLayoutGroup > ( ) ;
hl . childAlignment = TextAnchor . MiddleCenter ;
hl . spacing = 10f ;
hl . padding = new RectOffset ( 14 , 26 , 0 , 0 ) ;
hl . childControlWidth = true ;
hl . childControlHeight = false ;
hl . childForceExpandWidth = hl . childForceExpandHeight = false ;
var ic = UiKit . IconOrDot ( pill , icon , mark , dot , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , 46f ) ;
if ( ic ! = null )
2026-11-05 15:22:54 +00:00
{
2026-11-12 13:41:33 +00:00
var le = ic . gameObject . AddComponent < LayoutElement > ( ) ;
le . minWidth = le . preferredWidth = 46f ;
}
var t = UiKit . Label ( pill , text , 32 , ink , TextAnchor . MiddleLeft , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , new Vector2 ( 10f , 50f ) ) ;
t . horizontalOverflow = HorizontalWrapMode . Overflow ;
}
/// <summary>개발 파견 칸에 배치 영웅이 보이는지(테스트 · 왼쪽 아래 큰 상반신은 PD 2026-11-12 지시로 없앰).</summary>
public bool DevHeroShown { get ; private set ; }
static void ArrowBtn ( RectTransform scroll , string name , string mark , float x , float y , System . Action onClick )
{
var b = UiKit . Rect ( name , scroll , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( x , - y ) , new Vector2 ( 84f , 84f ) ) ;
var img = UiKit . Skin ( b , "circle122" , UiKit . Col ( "menu_btn" , UiKit . Dark ) , true ) ;
img . preserveAspect = true ;
b . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ( ) = > onClick ( ) ) ;
Stroke ( UiKit . FillLabel ( b , mark , 36 , UiKit . White , TextAnchor . MiddleCenter , true , 0f ) ) ;
2026-11-05 15:22:54 +00:00
}
static void Line ( RectTransform parent , Vector2 a , Vector2 b , Color c )
{
var mid = ( a + b ) / 2f ;
var dir = b - a ;
2026-11-12 13:41:33 +00:00
var rt = UiKit . Rect ( "Link" , parent , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , mid , new Vector2 ( dir . magnitude , 6f ) ) ;
2026-11-05 15:22:54 +00:00
rt . localRotation = Quaternion . Euler ( 0f , 0f , Mathf . Atan2 ( dir . y , dir . x ) * Mathf . Rad2Deg ) ;
UiKit . Paint ( rt , c , false ) ;
}
public void NodePopup ( string id )
{
var db = S . Db ;
var d = S . Save ;
var n = db . Dev . Get ( id ) ;
if ( n = = null ) return ;
int lv = DevRules . Level ( d , id ) ;
var sb = new System . Text . StringBuilder ( ) ;
sb . Append ( BranchKo ( n . branch ) ) . Append ( n . Big ? " · 큰 노드" : "" ) . Append ( " Lv " ) . Append ( lv ) . Append ( " / " ) . Append ( n . lvMax ) . Append ( "\n\n" ) ;
foreach ( var k in n . EffectKeys )
{
string what ;
DevRules . Keys . TryGetValue ( k , out what ) ;
sb . Append ( what ) . Append ( " " ) . Append ( ( n . pctPerLv * lv ) . ToString ( "0.##" ) ) . Append ( " → " ) . Append ( ( n . pctPerLv * Mathf . Min ( n . lvMax , lv + 1 ) ) . ToString ( "0.##" ) ) . Append ( "\n" ) ;
}
if ( n . requires . Count > 0 )
{
sb . Append ( "\n선행: " ) ;
for ( int i = 0 ; i < n . requires . Count ; i + + )
{
var r = n . requires [ i ] ;
bool ok = DevRules . Level ( d , r . node ) > = r . lv ;
sb . Append ( i > 0 ? " · " : "" ) . Append ( ok ? "<color=#3a6a30>" : "<color=#a03a22>" ) . Append ( db . Dev . Get ( r . node ) . nameKo ) . Append ( " Lv " ) . Append ( r . lv ) . Append ( "</color>" ) ;
}
sb . Append ( "\n" ) ;
}
if ( lv < n . lvMax )
{
int gold , bp ;
DevRules . NextCost ( n , lv , out gold , out bp ) ;
sb . Append ( "\n다음 비용 골드 " ) . Append ( gold . ToString ( "N0" ) ) . Append ( bp > 0 ? " · 도면 " + bp : "" ) ;
}
var why = DevRules . CanUpgrade ( db , d , id ) ;
Root . Modal ( n . nameKo , sb . ToString ( ) ,
new OutGameRoot . ModalButton ( why = = null ? "개발 Lv+1" : why , why = = null ? UiKit . Red : UiKit . Disabled , ( ) = >
{
var w = S . Do ( ( ) = > DevRules . Upgrade ( S . Db , S . Save , id ) ) ;
if ( w = = null ) NodePopup ( id ) ;
else Root . Toast ( w ) ;
} , true ) ,
new OutGameRoot . ModalButton ( "닫기" , UiKit . Dark , null ) ) ;
}
public static string BranchKo ( string b )
{
switch ( b )
{
case "commerce" : return "상업" ;
case "agri" : return "농업" ;
case "industry" : return "공업" ;
}
return b ;
}
static string BranchGoal ( string b )
{
switch ( b )
{
case "commerce" : return "재화 획득 효율" ;
case "agri" : return "자원 획득 효율" ;
case "industry" : return "장비 획득" ;
}
return "" ;
}
}
}