2026-11-05 15:22:54 +00:00
using System.Collections.Generic ;
using UnityEngine ;
using UnityEngine.UI ;
namespace TKS.OutGame
{
/// <summary>
/// 내정(하단 탭 · 기획 S9~S11 · 목업 img13·05·14 · PD #1093 C) — 상단 탭 3(한 항목 = 한 페이지):
/// ① 훈련 = 병종 탭 5 · 랭크 바(현재 랭크 마커 + 랭크 보너스 · 다음 랭크 승급 골드 · 남은 레벨) · 카드 3(HP·공·방 Lv n/Max · 골드) · 3항목 Max → 랭크 승급
/// ② 순찰 = 기준 챕터 · 누적 n/최대 시간 · 시간당 수확 4줄 · 쌓인 보상 4칸 · 수확 · 광고 2배(스텁) · 즉시 수확(보옥)
/// ③ 개발 = 갈래 ◀ 상업·농업·공업 ▶ 한 페이지씩 · 세로 트리(선행 조건 깊이로 줄 · 노드 n/Max · 큰 노드) · 노드 탭 = 강화 팝업 · 도면·골드 · 트리 초기화.
/// </summary>
public class DomesticScreen : OutGameScreen
{
public static readonly string [ ] PageNames = { "훈련" , "순찰" , "개발" } ;
int page ;
int typeIdx ;
int branchIdx ;
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 ; }
public override void Build ( )
{
Tab = 2 ;
Header ( "내정" , "훈련 · 순찰 · 개발" , true ) ;
var body = Body ( ) ;
for ( int i = 0 ; i < PageNames . Length ; i + + )
{
int k = i ;
tabs . Add ( 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 ) ) ) ;
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 ( ) ;
}
public override void Refresh ( )
{
for ( int i = 0 ; i < tabs . Count ; i + + )
{
bool on = i = = page ;
tabs [ i ] . GetComponent < Image > ( ) . color = on ? UiKit . Dark : UiKit . Paper ;
UiKit . LabelOf ( tabs [ i ] ) . color = on ? UiKit . White : UiKit . Ink ;
pages [ i ] . gameObject . SetActive ( on ) ;
}
UiKit . Clear ( pages [ page ] ) ;
if ( page = = 0 ) BuildTraining ( pages [ 0 ] ) ;
else if ( page = = 1 ) BuildPatrol ( pages [ 1 ] ) ;
else BuildDev ( pages [ 2 ] ) ;
}
// ---------- ① 훈련 ----------
void BuildTraining ( RectTransform p )
{
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 ;
UiKit . Btn ( "Type" + i , p , UnitTypeInfo . Mark ( ut ) , 40 , 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 ) ) ;
}
int rank = TrainingRules . Rank ( d , t ) ;
var stage = UiKit . TopBand ( "Unit" , p , 100f , 470f , 24f , 24f ) ;
UiKit . Paint ( stage , new Color ( 0.24f , 0.19f , 0.25f ) ) ;
UiKit . Label ( stage , TrainingRules . RankName ( db , rank ) + " <size=40>랭크 " + rank + "/" + db . Training . MaxRank + "</size>" , 60 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 20f ) , new Vector2 ( 900f , 90f ) ) ;
var halo = UiKit . Dot ( "Halo" , stage , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - 40f ) , 300f , new Color ( 1f , 1f , 1f , 0.1f ) ) ;
halo . raycastTarget = false ;
var face = UiKit . Dot ( "Face" , stage , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - 40f ) , 220f , UiKit . TypeColor ( t ) ) ;
UiKit . FillLabel ( face . transform , UnitTypeInfo . Mark ( t ) , 110 , UiKit . White ) ;
UiKit . Label ( stage , UnitTypeInfo . Korean ( t ) + " 영웅·병사 전원" , 30 , new Color ( 0.85f , 0.8f , 0.86f ) , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 14f ) , new Vector2 ( 900f , 50f ) , false ) ;
// 랭크 바: 현재 랭크 마커 + 랭크 보너스 · 다음 랭크 승급 비용 · 남은 레벨
var bar = UiKit . TopBand ( "RankBar" , p , 586f , 230f , 24f , 24f ) ;
UiKit . Paint ( bar , new Color ( 0.17f , 0.14f , 0.19f ) ) ;
UiKit . Label ( bar , "랭크 진행" , 30 , new Color ( 0.85f , 0.8f , 0.86f ) , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f , - 12f ) , new Vector2 ( 300f , 50f ) ) ;
int left = TrainingRules . Remaining ( db , d , t ) ;
int total = db . Training . MaxLevel * TrainingTable . Stats . Length ;
var fill = UiKit . Bar ( "Progress" , bar , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 24f , 6f ) , new Vector2 ( 760f , 30f ) , new Color ( 0.32f , 0.28f , 0.35f ) , new Color ( 0.95f , 0.68f , 0.2f ) ) ;
float v = total > 0 ? ( total - left ) / ( float ) total : 0f ;
UiKit . SetBar ( fill , v ) ;
var marker = UiKit . Dot ( "Marker" , bar , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 24f + 760f * v , 6f ) , 70f , new Color ( 0.95f , 0.68f , 0.2f ) ) ;
UiKit . FillLabel ( marker . transform , "▲" , 34 , UiKit . Ink ) ;
var r = db . Training . RankOf ( rank ) ;
UiKit . Label ( bar , TrainingRules . RankName ( db , rank ) + " · 랭크 보너스 +" + ( r ! = null ? r . rankPct : 0f ) . ToString ( "0.#" ) + "%" , 32 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 24f , 16f ) , new Vector2 ( 700f , 54f ) ) ;
string promo ;
if ( rank > = db . Training . MaxRank ) promo = "최종 랭크" ;
else promo = "다음 랭크 승급 비용\n<size=40>골드 × " + TrainingRules . PromoteCost ( db , d , t ) . ToString ( "N0" ) + "</size>" ;
UiKit . Label ( bar , promo , 26 , new Color ( 0.85f , 0.8f , 0.86f ) , TextAnchor . UpperRight , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( - 24f , - 12f ) , new Vector2 ( 300f , 110f ) , false ) ;
UiKit . Label ( bar , left + "\n<size=24>남은 레벨</size>" , 40 , UiKit . White , TextAnchor . LowerRight , new Vector2 ( 1f , 0f ) , new Vector2 ( 1f , 0f ) , new Vector2 ( - 24f , 12f ) , new Vector2 ( 200f , 90f ) ) ;
// 카드 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 ;
var card = UiKit . Box ( "Card" + i , p , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f + i * ( cw + 21f ) , - 838f ) , new Vector2 ( cw , 360f ) , max ? new Color ( 0.85f , 0.81f , 0.9f ) : UiKit . Paper ) ;
UiKit . Border ( card , UiKit . Line , 3f ) ;
var head = UiKit . TopBand ( "Head" , card . transform , 0f , 72f ) ;
UiKit . Paint ( head , max ? UiKit . Purple : new Color ( 0.93f , 0.87f , 0.7f ) ) ;
UiKit . FillLabel ( head , max ? "Max" : "LV." + lv , 36 , max ? UiKit . White : UiKit . Ink ) ;
var dot = UiKit . Dot ( "Icon" , card . transform , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 86f ) , 96f , new Color ( 0.9f , 0.86f , 0.8f ) ) ;
UiKit . FillLabel ( dot . transform , marks [ i ] , 40 , UiKit . Ink ) ;
UiKit . Label ( card . transform , names [ i ] , 38 , UiKit . Ink , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 190f ) , new Vector2 ( 310f , 50f ) ) ;
UiKit . Label ( card . transform , "LV " + lv + "/" + db . Training . MaxLevel + " +" + TrainingRules . Bonus ( db , d , t , TrainingTable . Stats [ i ] ) . ToString ( "0.#" ) + "%" , 26 , UiKit . InkSoft , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 232f ) , new Vector2 ( 310f , 36f ) , false ) ;
if ( max ) UiKit . Label ( card . transform , "<color=#3a8a30>✔</color>" , 48 , UiKit . Ok , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 20f ) , new Vector2 ( 300f , 70f ) ) ;
else
{
var b = UiKit . Btn ( "Up" , card . transform , TrainingRules . Cost ( db , d , t , i ) . ToString ( "N0" ) + " 골드" , 30 , UiKit . Coin , UiKit . Ink , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 14f ) , new Vector2 ( 280f , 66f ) , ( ) = >
{
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 )
{
var pb = UiKit . Btn ( "Promote" , p , "랭크 승급 → " + TrainingRules . RankName ( db , rank + 1 ) + " · 골드 " + TrainingRules . PromoteCost ( db , d , t ) . ToString ( "N0" ) , 36 , UiKit . Red , UiKit . White , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 90f ) , new Vector2 ( 900f , 110f ) , ( ) = >
{
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 ;
}
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.#" ) + "% (3항목 Max → 랭크 승급 · 레벨 리셋 · 랭크 보너스 유지)" , 26 , UiKit . InkSoft , TextAnchor . MiddleCenter ,
new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 20f ) , new Vector2 ( 1030f , 60f ) , false ) ;
}
// ---------- ② 순찰 ----------
void BuildPatrol ( RectTransform p )
{
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 ) ;
var basis = Panel ( p , "Basis" , 0f , 110f ) ;
UiKit . Label ( basis , "기준: 마지막 클리어 챕터 " + ( row ! = null ? row . chapter : 0 ) + "장" , 38 , UiKit . Ink , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f , - 8f ) , new Vector2 ( 900f , 54f ) ) ;
UiKit . Label ( basis , "제" + Mathf . Max ( 1 , d . chapterMax ) + "장 · 시간당 수확 · 오프라인 누적" , 28 , UiKit . InkSoft , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 24f , 8f ) , new Vector2 ( 900f , 44f ) , false ) ;
var acc = Panel ( p , "Accum" , 126f , 170f ) ;
UiKit . Label ( acc , "누적 " + FormatHours ( h ) + " / 최대 " + FormatHours ( cap ) , 40 , UiKit . Ink , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f , - 8f ) , new Vector2 ( 900f , 56f ) ) ;
var fill = UiKit . Bar ( "Bar" , acc , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - 6f ) , new Vector2 ( 980f , 34f ) , new Color ( 0.88f , 0.84f , 0.77f ) , UiKit . Red ) ;
UiKit . SetBar ( fill , cap > 0f ? h / cap : 0f ) ;
UiKit . Label ( acc , "성장 패스 24시간(준비 중) · 농업 개발로 +" + Mathf . RoundToInt ( DevRules . Mod ( db , d , DevRules . PatrolCap ) ) + "분" , 26 , UiKit . Red , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 24f , 8f ) , new Vector2 ( 900f , 44f ) , false ) ;
var rate = Panel ( p , "Rate" , 312f , 400f ) ;
UiKit . Label ( rate , "시간당 수확" , 38 , UiKit . Ink , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f , - 8f ) , new Vector2 ( 600f , 56f ) ) ;
string [ ] rn = { "골드" , "병사 경험" , "승급 장비" , "보물 청동 상자" } ;
string [ ] rv =
{
PatrolRules . GoldPerHour ( db , d ) . ToString ( "N0" ) + " / 시간" ,
PatrolRules . ExpPerHour ( db , d ) . ToString ( "N0" ) + " / 시간(출전 편성)" ,
UiKit . GradeKo ( PatrolRules . GearGrade ( db , d ) ) + " · " + PatrolRules . GearPerHour ( db , d ) . ToString ( "0.##" ) + " / 시간" ,
PatrolRules . ChestPerHour ( db , d ) . ToString ( "0.##" ) + " / 시간" ,
} ;
Color [ ] rc = { UiKit . Coin , new Color ( 0.8f , 0.48f , 0.38f ) , UiKit . Ok , new Color ( 0.7f , 0.48f , 0.3f ) } ;
2026-11-06 14:20:57 +00:00
string [ ] rIcon = { "cur_gold" , "cur_exp" , "cur_gear" , "chest_bronze" } ;
2026-11-05 15:22:54 +00:00
for ( int i = 0 ; i < 4 ; i + + )
{
var line = UiKit . Box ( "Row" + i , rate , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 76f - i * 78f ) , new Vector2 ( 980f , 68f ) , new Color ( 0.95f , 0.92f , 0.86f ) ) ;
2026-11-06 14:20:57 +00:00
if ( UiKit . Icon ( line . transform , rIcon [ i ] , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 12f , 0f ) , 58f ) = = null )
UiKit . Dot ( "Dot" , line . transform , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 16f , 0f ) , 50f , rc [ i ] ) ;
2026-11-05 15:22:54 +00:00
UiKit . Label ( line . transform , rn [ i ] , 34 , UiKit . Ink , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 86f , 0f ) , new Vector2 ( 400f , 56f ) ) ;
UiKit . Label ( line . transform , rv [ i ] , 30 , UiKit . Ink , TextAnchor . MiddleRight , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 20f , 0f ) , new Vector2 ( 520f , 56f ) , false ) ;
}
var y = PatrolRules . Preview ( db , d , h , 1f ) ;
var got = Panel ( p , "Stored" , 728f , 220f ) ;
UiKit . Label ( got , "쌓인 보상" , 38 , UiKit . Ink , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f , - 8f ) , new Vector2 ( 600f , 56f ) ) ;
string [ ] sv = { "골드\n" + y . gold . ToString ( "N0" ) , "병사 경험\n" + y . exp . ToString ( "N0" ) , "승급 장비\n" + y . gear , "청동 상자\n" + y . chests } ;
for ( int i = 0 ; i < 4 ; i + + )
{
var chip = UiKit . Box ( "Chip" + i , got , new Vector2 ( 0f , 0f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 24f + i * 250f , 16f ) , new Vector2 ( 232f , 130f ) , new Color ( 0.95f , 0.92f , 0.86f ) ) ;
UiKit . Border ( chip , rc [ i ] , 3f ) ;
UiKit . FillLabel ( chip . transform , sv [ i ] , 30 , UiKit . Ink ) ;
}
var hb = UiKit . Btn ( "Harvest" , p , "수확" , 64 , UiKit . Red , UiKit . White , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 966f ) , new Vector2 ( 1030f , 140f ) , ( ) = > DoHarvest ( false ) ) ;
hb . interactable = h > 0f ;
EconomyRules . RollDaily ( d , S . Now ) ;
var ad = UiKit . Btn ( "Ad" , p , "광고 2배 수확\n<size=26>" + d . adsUsed . patrol + "/" + e . PatrolAdDaily + "회/일 (시험)</size>" , 36 , UiKit . Dark , UiKit . White , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 24f , - 1124f ) , new Vector2 ( 505f , 130f ) , ( ) = > DoHarvest ( true ) ) ;
ad . interactable = h > 0f & & d . adsUsed . patrol < e . PatrolAdDaily ;
UiKit . Btn ( "Instant" , p , "즉시 수확 " + FormatHours ( e . PatrolInstantHours ) + "분\n<size=26>보옥 " + e . PatrolInstantGem + "</size>" , 36 , UiKit . Jade , UiKit . White , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( - 24f , - 1124f ) , new Vector2 ( 505f , 130f ) , ( ) = >
{
string why ;
var r = S . InstantHarvest ( out why ) ;
if ( r = = null ) Root . Toast ( why ) ;
else ShowYield ( "즉시 수확" , r ) ;
} ) ;
}
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" ) ;
sb . Append ( "골드 +" ) . Append ( y . gold . ToString ( "N0" ) ) . Append ( "\n병사 경험 +" ) . Append ( y . exp ) . Append ( " (" ) . Append ( y . expTo . Count ) . Append ( "명)" ) ;
if ( y . gearIds . Count > 0 )
{
var names = new List < string > ( ) ;
foreach ( var g in y . gearIds ) names . Add ( S . Db . Gear . Get ( g ) . nameKo ) ;
sb . Append ( "\n장비 " ) . Append ( string . Join ( " · " , names . ToArray ( ) ) ) ;
}
if ( y . chestIds . Count > 0 ) sb . Append ( "\n" ) . Append ( S . Db . Chests . Get ( y . chestIds [ 0 ] ) . nameKo ) . Append ( " × " ) . Append ( y . chestIds . Count ) ;
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 ) + "분" : "" ) ;
}
static RectTransform Panel ( RectTransform p , string name , float top , float h )
{
var band = UiKit . TopBand ( name , p , top , h , 24f , 24f ) ;
var img = UiKit . Paint ( band , UiKit . Paper ) ;
UiKit . Border ( img , UiKit . Line , 3f ) ;
return band ;
}
// ---------- ③ 개발 ----------
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 ;
UiKit . Btn ( "Prev" , p , "◀" , 44 , UiKit . Paper , UiKit . Ink , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( - 330f , - 6f ) , new Vector2 ( 100f , 90f ) , ( ) = > SetBranch ( branchIdx - 1 ) ) ;
UiKit . Btn ( "Next" , p , "▶" , 44 , UiKit . Paper , UiKit . Ink , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 330f , - 6f ) , new Vector2 ( 100f , 90f ) , ( ) = > SetBranch ( branchIdx + 1 ) ) ;
UiKit . Label ( p , BranchKo ( branches [ ( branchIdx + n - 1 ) % n ] ) , 24 , UiKit . InkSoft , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( - 330f , - 96f ) , new Vector2 ( 140f , 34f ) , false ) ;
UiKit . Label ( p , BranchKo ( branches [ ( branchIdx + 1 ) % n ] ) , 24 , UiKit . InkSoft , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 330f , - 96f ) , new Vector2 ( 140f , 34f ) , false ) ;
var title = UiKit . Box ( "Branch" , p , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 6f ) , new Vector2 ( 420f , 90f ) , UiKit . Red ) ;
UiKit . FillLabel ( title . transform , BranchKo ( br ) , 48 , UiKit . White ) ;
for ( int i = 0 ; i < n ; i + + ) UiKit . Dot ( "Page" + i , p , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( ( i - ( n - 1 ) / 2f ) * 30f , - 106f ) , 16f , i = = branchIdx ? UiKit . Red : UiKit . Slot ) ;
UiKit . Label ( p , BranchKo ( br ) + " — " + BranchGoal ( br ) , 30 , UiKit . InkSoft , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 136f ) , new Vector2 ( 900f , 44f ) , false ) ;
// 깊이(선행 조건) = 줄 · 같은 줄 = 가로로
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 ) ;
}
var tree = UiKit . Fill ( "Tree" , p , 24f , 24f , 190f , 190f ) ;
float rowH = Mathf . Min ( 190f , 1050f / ( maxDepth + 1 ) ) ;
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 ) ;
for ( int i = 0 ; i < row . Count ; i + + )
pos [ row [ i ] . id ] = new Vector2 ( ( i - ( row . Count - 1 ) / 2f ) * ( row . Count > 1 ? 840f / ( row . Count - 1 ) : 0f ) , - rowH * ( dp + 0.5f ) ) ;
}
foreach ( var nd in nodes )
foreach ( var r in nd . requires )
if ( pos . ContainsKey ( r . node ) ) Line ( tree , pos [ r . node ] , pos [ nd . id ] , DevRules . Level ( d , r . node ) > = r . lv ? new Color ( 0.45f , 0.36f , 0.3f ) : UiKit . Slot ) ;
NodeCount = 0 ;
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 ;
float size = nd . Big ? Mathf . Min ( 150f , rowH * 0.78f ) : Mathf . Min ( 110f , rowH * 0.6f ) ;
var c = UiKit . Btn ( "Node_" + nd . id , tree , "" , 30 , max ? UiKit . Coin : open ? UiKit . Paper : new Color ( 0.55f , 0.51f , 0.48f ) , UiKit . Ink , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , pos [ nd . id ] , new Vector2 ( size , size ) , ( ) = > NodePopup ( node . id ) ) ;
var img = c . GetComponent < Image > ( ) ;
img . sprite = UiKit . Circle ;
img . type = Image . Type . Simple ;
UiKit . Border ( img , nd . Big ? new Color ( 0.78f , 0.62f , 0.25f ) : UiKit . Line , nd . Big ? 6f : 3f ) ;
UiKit . LabelOf ( c ) . text = open ? nd . nameKo . Substring ( 0 , 1 ) : "잠" ;
UiKit . LabelOf ( c ) . fontSize = nd . Big ? 60 : 44 ;
UiKit . LabelOf ( c ) . color = open ? ( max ? UiKit . White : UiKit . Ink ) : new Color ( 0.85f , 0.82f , 0.78f ) ;
var pill = UiKit . Box ( "Lv" , c . transform , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 110f , 40f ) , UiKit . Dark ) ;
UiKit . FillLabel ( pill . transform , lv + "/" + nd . lvMax , 24 , UiKit . White ) ;
float side = pos [ nd . id ] . x < = 0f ? 1f : - 1f ;
var lab = UiKit . Label ( tree , nd . nameKo , 28 , open ? UiKit . Ink : UiKit . InkSoft , side > 0 ? TextAnchor . MiddleLeft : TextAnchor . MiddleRight , new Vector2 ( 0.5f , 1f ) ,
new Vector2 ( side > 0 ? 0f : 1f , 0.5f ) , pos [ nd . id ] + new Vector2 ( side * ( size / 2f + 10f ) , 0f ) , new Vector2 ( 240f , 50f ) ) ;
lab . horizontalOverflow = HorizontalWrapMode . Overflow ;
NodeCount + + ;
}
var foot = UiKit . Box ( "Foot" , p , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 16f ) , new Vector2 ( 1032f , 150f ) , UiKit . Paper ) ;
UiKit . Border ( foot , UiKit . Line , 3f ) ;
2026-11-06 14:20:57 +00:00
UiKit . IconOrDot ( foot . transform , "cur_blueprint" , "도" , new Color ( 0.33f , 0.45f , 0.62f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 20f , 0f ) , 76f ) ;
2026-11-05 15:22:54 +00:00
UiKit . Label ( foot . transform , "도면 × " + d . inv . blueprint , 38 , UiKit . Ink , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 110f , 0f ) , new Vector2 ( 260f , 60f ) ) ;
2026-11-06 14:20:57 +00:00
UiKit . IconOrDot ( foot . transform , "cur_gold" , "골" , UiKit . Coin , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 380f , 0f ) , 76f ) ;
2026-11-05 15:22:54 +00:00
UiKit . Label ( foot . transform , d . gold . ToString ( "N0" ) , 38 , UiKit . Ink , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 470f , 0f ) , new Vector2 ( 220f , 60f ) ) ;
UiKit . Btn ( "Reset" , foot . transform , "트리 초기화\n<size=24>보옥 " + db . Economy . DevResetGem + " · 도면 환급</size>" , 34 , UiKit . Dark , UiKit . White , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 1f , 0.5f ) , new Vector2 ( - 16f , 0f ) , new Vector2 ( 320f , 120f ) , ( ) = >
{
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 ) ) ;
} ) ;
}
static void Line ( RectTransform parent , Vector2 a , Vector2 b , Color c )
{
var mid = ( a + b ) / 2f ;
var dir = b - a ;
var rt = UiKit . Rect ( "Link" , parent , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , mid , new Vector2 ( dir . magnitude , 8f ) ) ;
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 "" ;
}
public override bool OnBack ( )
{
return false ;
}
}
}