2026-11-05 15:22:54 +00:00
using System.Collections.Generic ;
using UnityEngine ;
using UnityEngine.UI ;
namespace TKS.OutGame
{
/// <summary>
/// 보물(하단 탭 · 기획 S8 · 목업 img07·16·17 · 카피바라 수집품 구조 · PD #1093 C) — 상단 탭 3:
/// ① 보유 중 = 그리드 4열 · 등급 색 · ★ · 미보유 회색 · 탭 = 상세(기본 효과 · 조건 효과 n/max · 강화 ★)
/// ② 세트 = 세트마다 구성 · 조각 게이지 · [세트]/[3성]/[6성]/[10성] 효과 ③ 상자 = 상자 포인트 게이지 → 명장 상자 · 선택 상자 + 확률 · 보유 수 · 열기(상자는 이 탭에서만 연다).
/// </summary>
public class TreasureScreen : OutGameScreen
{
public static readonly string [ ] PageNames = { "보유 중" , "세트" , "상자" } ;
int page ;
int chestIdx ;
2026-11-09 09:23:18 +00:00
bool chestPicked ;
2026-11-05 15:22:54 +00:00
readonly List < Button > tabs = new List < Button > ( ) ;
readonly List < RectTransform > pages = new List < RectTransform > ( ) ;
2026-11-13 07:49:11 +00:00
/// <summary>배경 장면(PD 2026-11-13 · vault = 보물고 · chest = 금고) — 처음 볼 때 만들고 숨김/보임만.</summary>
class Scene
{
public List < LobbyScene . Row > rows ;
public LobbyAmbient amb ;
public RectTransform art ;
public float artW ;
public float artH ;
}
RectTransform backdrop ;
readonly Dictionary < string , Scene > scenes = new Dictionary < string , Scene > ( ) ;
Image openChestIcon ;
/// <summary>지금 보이는 배경 장면 이름(그림이 없으면 null · 테스트).</summary>
public string ShownScene { get ; private set ; }
/// <summary>보유 중·세트 칸을 진열대(칸 틀 + 선반)로 그렸는가(테스트).</summary>
public bool ShelfCells { get ; private set ; }
/// <summary>상자 탭을 금고 받침대 배치로 그렸는가(테스트).</summary>
public bool ChestOnPedestal { get ; private set ; }
/// <summary>받침대 위 상자 아이콘(테스트).</summary>
public Image PedestalChest
{
get { return openChestIcon ; }
}
2026-11-05 15:22:54 +00:00
public int Page
{
get { return page ; }
}
public int GridCount { get ; private set ; }
public int SetCount { get ; private set ; }
public ChestResult LastOpen { get ; private set ; }
public string SelectedChest
{
get
{
var l = S . Db . Chests . chests ;
return l . Count > 0 ? l [ Mathf . Clamp ( chestIdx , 0 , l . Count - 1 ) ] . id : "" ;
}
}
public override void Build ( )
{
Tab = 1 ;
2026-11-09 09:12:53 +00:00
if ( UISkin . On ) UiKit . Paint ( UiKit . Fill ( "ListBg" , Rt ) , UiKit . Col ( "list_bg" , UiKit . Bg ) , false ) ; // 목록형 바탕(Fable §5-3)
2026-11-09 03:20:32 +00:00
Header ( "보물" , "" , true ) ; // 부제 삭제(설계서 §2-5)
2026-11-05 15:22:54 +00:00
var body = Body ( ) ;
2026-11-13 07:49:11 +00:00
backdrop = UiKit . Fill ( "Backdrop" , body ) ; // 배경 장면(보물고 · 금고 · 탭·페이지 뒤 · PD 2026-11-13)
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 ) ) ;
}
}
2026-11-09 09:12:53 +00:00
/// <summary>새 상자 화면 부품(Fable §4-5)이 테마에 있는가.</summary>
static bool Kit
{
get { return UISkin . Has ( "card3_bg" ) & & UISkin . Has ( "banner_point" ) & & UISkin . Has ( "btn_l_yellow" ) ; }
}
/// <summary>보물 아이콘 id — 납품 그림(tre_<id>)이 있으면 그것 · 없으면 기본 스탯별 팩 임시 아이콘(tre_stat_* · Fable §6 N3 전까지).</summary>
static string IconId ( TreasureDb . Item it )
{
if ( UiKit . HasFinalIcon ( "tre_" + it . id ) ) return "tre_" + it . id ;
return UISkin . Icon ( "tre_stat_" + it . baseStat ) ! = null ? "tre_stat_" + it . baseStat : null ;
}
2026-11-05 15:22:54 +00:00
public void SetPage ( int p )
{
page = Mathf . Clamp ( p , 0 , pages . Count - 1 ) ;
Refresh ( ) ;
}
public void SelectChest ( string id )
{
int i = S . Db . Chests . IndexOf ( id ) ;
if ( i > = 0 ) chestIdx = i ;
2026-11-09 09:23:18 +00:00
chestPicked = true ;
2026-11-05 15:22:54 +00:00
Refresh ( ) ;
}
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
{
tabs [ i ] . GetComponent < Image > ( ) . color = on ? UiKit . Red : UiKit . Paper ; // 선택 = 붉은 옻칠(다른 화면 탭과 같음)
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-13 07:49:11 +00:00
var scene = ShowScene ( TreasureScene . PageScene [ Mathf . Clamp ( page , 0 , TreasureScene . PageScene . Length - 1 ) ] ) ;
2026-11-05 15:22:54 +00:00
UiKit . Clear ( pages [ page ] ) ;
2026-11-13 07:49:11 +00:00
openChestIcon = null ;
ShelfCells = false ;
ChestOnPedestal = false ;
2026-11-05 15:22:54 +00:00
if ( page = = 0 ) BuildOwned ( pages [ 0 ] ) ;
else if ( page = = 1 ) BuildSets ( pages [ 1 ] ) ;
2026-11-13 07:49:11 +00:00
else if ( scene ! = null & & Kit ) BuildChestsVault ( pages [ 2 ] , scene ) ;
2026-11-09 09:12:53 +00:00
else if ( Kit ) BuildChestsKit ( pages [ 2 ] ) ;
2026-11-05 15:22:54 +00:00
else BuildChests ( pages [ 2 ] ) ;
}
2026-11-13 07:49:11 +00:00
/// <summary>그 장면을 (처음이면 만들고) 보이고 나머지는 숨김 · 그림이 없으면 null(예전 바탕).</summary>
Scene ShowScene ( string name )
{
Scene s ;
if ( ! scenes . TryGetValue ( name , out s ) )
{
s = new Scene { rows = TreasureScene . Scene ( name ) } ;
s . amb = DomesticScene . Backdrop ( backdrop , "Scene_" + name , s . rows , out s . art , out s . artW , out s . artH ) ;
scenes [ name ] = s ;
}
foreach ( var kv in scenes )
if ( kv . Value . art ! = null )
kv . Value . art . parent . gameObject . SetActive ( kv . Key = = name ) ;
ShownScene = s . art ! = null ? name : null ;
return s . art ! = null ? s : null ;
}
/// <summary>보물고 진열장 배경이 보일 때 그 장면 표 줄(아니면 null = 예전 배치).</summary>
LobbyScene . Row VaultRow ( string kind )
{
Scene s ;
return ShownScene = = "vault" & & scenes . TryGetValue ( "vault" , out s ) ? DomesticScene . First ( s . rows , kind ) : null ;
}
/// <summary>장면 그림 px → 페이지 좌표(본문 = 1080 폭 설계 단위 · 페이지 = 본문 위 118 아래 · view f_y 줄이 본문 맨 위 · f_scale 배).</summary>
static Vector2 ArtToPage ( Scene s , float x , float y )
{
var view = DomesticScene . First ( s . rows , "view" ) ;
float vy = view ! = null ? view . y : 0f ;
float z = view ! = null & & view . scale > 0.01f ? view . scale : 1f ;
float k = OutGameRoot . Reference . x * z / Mathf . Max ( 1f , s . artW ) ; // 그림 px → 본문 px
return new Vector2 ( OutGameRoot . Reference . x * 0.5f + ( x - s . artW * 0.5f ) * k , ( y - vy ) * k - 118f ) ;
}
2026-11-09 03:20:32 +00:00
/// <summary>보물 등급 대표 색(테마 tre_<등급> · 희귀 청 · 영웅 자 · 전설 주 · 신화 적 · 명장 금).</summary>
2026-11-05 15:22:54 +00:00
public static Color GradeColor ( string g )
{
2026-11-09 03:20:32 +00:00
return UiKit . Col ( "tre_" + g , UiKit . Slot ) ;
2026-11-05 15:22:54 +00:00
}
2026-11-09 03:20:32 +00:00
/// <summary>
/// 보물 칸(UI 상업용 재설계 ⑥ · 설계서 §3⑥) = 팩 ItemFrame 5층 등급색 + 아이콘(납품 tre_* · 없으면 이름 첫 글자) + ★ 또는 조각 바 + 아래 이름 판 ·
/// 미보유 = 진청 빈 칸 + 흰 25 % 실루엣.
/// </summary>
2026-11-12 14:29:32 +00:00
/// <summary>보물 칸 아래 이름 띠 높이(칸 아래 끝에서 · 아이콘 자리는 이 위 색 칸).</summary>
const float NameBandH = 62f ;
/// <summary>조각 막대·별 줄 윗변(칸 아래 끝에서 · 막대 y 76 + 높이 26 = 별 줄 y 72 + 30).</summary>
const float ShardsTop = 102f ;
2026-11-05 15:22:54 +00:00
void TreasureCell ( Transform parent , TreasureDb . Item it , bool showShards )
{
var st = S . Save . inv . Treasure ( it . id ) ;
bool owned = st ! = null ;
2026-11-09 03:20:32 +00:00
var cell = UiKit . Cell ( parent , "T_" + it . id , Color . clear , owned ? ( System . Action ) ( ( ) = > Detail ( it . id ) ) : null ) ;
cell . sprite = null ; // 바닥 = 클릭 판(투명)
var t = cell . transform ;
2026-11-13 07:49:11 +00:00
// 보물고 진열장 배경일 때 = 선반 진열(PD 2026-11-13 2차 「칸 틀이 튐 → 레퍼런스 게임 유물 목록처럼 가독성 · 내정처럼 공간 인지」) —
// 틀 없는 큰 아이콘이 이어진 선반 판 위에 서고, 선반 앞에 등급 색 리본 이름표
Scene vs ;
var shelfRow = ShownScene = = "vault" & & scenes . TryGetValue ( "vault" , out vs ) ? DomesticScene . First ( vs . rows , "shelf" ) : null ;
var ribbonRow = shelfRow ! = null ? DomesticScene . First ( scenes [ "vault" ] . rows , "ribbon" ) : null ;
var shelfSp = shelfRow ! = null ? UISkin . Icon ( shelfRow . sprite ) : null ;
var ribbonSp = ribbonRow ! = null ? UISkin . Icon ( ribbonRow . sprite ) : null ;
if ( shelfSp ! = null & & ribbonSp ! = null )
{
ShelfCells = true ;
ShelfCell ( t , it , owned , showShards , shelfRow , ribbonRow , shelfSp , ribbonSp ) ;
return ;
}
2026-11-09 03:20:32 +00:00
UiKit . ItemSlot ( t , owned ? "tre_" + it . grade + "_" : "tre_none_" , owned ) ;
2026-11-12 14:29:32 +00:00
// 아이콘 자리 = 이름 띠 위 색 칸 전체 → 아이콘은 그 정가운데(PD 2026-11-11 「정중앙 · 20 % 키움」 · 전에는 위에서 100 px 고정 = 14 px 위로 뜸)
// 세트 화면(조각 막대 표시)은 막대 위부터 — 커진 아이콘 아래를 막대가 가리지 않게
var art = UiKit . Rect ( "Art" , t , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , Vector2 . zero ) ;
art . anchorMin = Vector2 . zero ;
art . anchorMax = Vector2 . one ;
art . offsetMin = new Vector2 ( 0f , showShards ? ShardsTop : NameBandH ) ;
art . offsetMax = Vector2 . zero ;
2026-11-09 09:12:53 +00:00
string icon = IconId ( it ) ;
if ( icon ! = null )
2026-11-09 03:20:32 +00:00
{
2026-11-12 14:29:32 +00:00
// 아이콘 = 납품 그림 또는 기본 스탯별 임시(Fable §4-5 「글자 → 보물 아이콘」) · 미보유 = 흰 25 % 실루엣 · 크기 = 예전 140/120 × 1.2
var ic = UiKit . Icon ( art , icon , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , icon . StartsWith ( "tre_stat_" ) ? 144f : 168f ) ;
2026-11-09 03:20:32 +00:00
if ( owned ) UiKit . DropShadow ( ic , 5f ) ;
else ic . color = UiKit . Col ( "tre_none_icon" , UiKit . InkSoft ) ;
}
else if ( owned ) UiKit . Title ( UiKit . FillLabel ( art , it . nameKo . Substring ( 0 , 1 ) , 84 , UiKit . White ) , true ) ;
2026-11-13 07:49:11 +00:00
CellFooter ( t , it , st , owned , showShards ) ;
}
/// <summary>
/// 선반 진열 칸(PD 2026-11-13 2차 · 레퍼런스 게임 유물 목록 + 보물고 진열장) — 표 vault_shelf · vault_ribbon · vault_icon · vault_grid/setgrid:
/// 선반 판(칸 간격 절반씩 옆으로 = 한 줄이 이어진 선반) → 틀 없는 큰 아이콘(판 윗면에 섬 · 미보유 = 어둡게) → 선반 앞 등급 색 리본 이름표(미보유 = 어두운 리본) → 별·조각 막대(판 앞면).
/// </summary>
void ShelfCell ( Transform t , TreasureDb . Item it , bool owned , bool showShards , LobbyScene . Row shelfRow , LobbyScene . Row ribbonRow , Sprite shelfSp , Sprite ribbonSp )
{
var st = S . Save . inv . Treasure ( it . id ) ;
var rows = scenes [ "vault" ] . rows ;
var grid = DomesticScene . First ( rows , showShards ? "setgrid" : "grid" ) ;
var iconRow = DomesticScene . First ( rows , "icon" ) ;
float gapX = grid ! = null & & grid . gapMin > 0f ? grid . gapMin : 16f ;
float shelfY = shelfRow . y > 0f ? shelfRow . y : 58f ;
float shelfH = shelfRow . h > 0f ? shelfRow . h : 30f ;
float ribY = ribbonRow . y ;
float ribH = ribbonRow . h > 0f ? ribbonRow . h : 60f ;
var shelf = UiKit . Rect ( "Shelf" , t , Vector2 . zero , Vector2 . zero , Vector2 . zero , Vector2 . zero ) ;
shelf . anchorMin = Vector2 . zero ;
shelf . anchorMax = new Vector2 ( 1f , 0f ) ;
shelf . offsetMin = new Vector2 ( - gapX * 0.5f - 2f , shelfY ) ; // 옆 칸 판과 2 px 겹침 = 한 줄로 이어진 선반(QA r3 M3)
shelf . offsetMax = new Vector2 ( gapX * 0.5f + 2f , shelfY + shelfH ) ;
var si = shelf . gameObject . AddComponent < Image > ( ) ;
si . sprite = shelfSp ;
si . raycastTarget = false ;
if ( shelfSp . border . sqrMagnitude > 0f ) si . type = Image . Type . Sliced ;
string icon = IconId ( it ) ;
float size = iconRow ! = null ? ( showShards ? iconRow . h : iconRow . w ) : 160f ;
if ( size < 1f ) size = 160f ;
float stand = shelfY + shelfH * 0.6f ; // 아이콘 아래끝 = 판 윗면
if ( icon ! = null )
{
var ic = UiKit . Icon ( t , icon , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , stand ) , icon . StartsWith ( "tre_stat_" ) ? size * 0.86f : size ) ;
if ( owned ) UiKit . DropShadow ( ic , 5f ) ;
else
{
// 미보유 = 밝은 회색 틴트(표 vault_none · 어두운 판에 묻히지 않게 · QA r3 M1)
var none = DomesticScene . First ( rows , "none" ) ;
ic . color = none ! = null ? none . color : UiKit . Col ( "tre_none_icon" , UiKit . InkSoft ) ;
}
}
else if ( owned )
{
var art = UiKit . Rect ( "Art" , t , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , stand ) , new Vector2 ( size , size ) ) ;
UiKit . Title ( UiKit . FillLabel ( art , it . nameKo . Substring ( 0 , 1 ) , 84 , UiKit . White ) , true ) ;
}
var rib = UiKit . Rect ( "Ribbon" , t , Vector2 . zero , Vector2 . zero , Vector2 . zero , Vector2 . zero ) ;
rib . anchorMin = Vector2 . zero ;
rib . anchorMax = new Vector2 ( 1f , 0f ) ;
rib . offsetMin = new Vector2 ( - 4f , ribY ) ;
rib . offsetMax = new Vector2 ( 4f , ribY + ribH ) ;
var ri = rib . gameObject . AddComponent < Image > ( ) ;
ri . sprite = ribbonSp ;
ri . raycastTarget = false ;
if ( ribbonSp . border . sqrMagnitude > 0f ) ri . type = Image . Type . Sliced ;
ri . color = owned ? GradeColor ( it . grade ) : UiKit . Col ( "tre_none_ribbon" , new Color ( 0.36f , 0.33f , 0.36f , 1f ) ) ;
int fs = ribbonRow . scale > 1f ? Mathf . RoundToInt ( ribbonRow . scale ) : 24 ;
// 미보유도 유물 이름(흐린 글자 · 레퍼런스 목록처럼 무엇인지 보임 · QA r3 M7) — 미보유 = 어두운 리본 + 흐린 아이콘으로 구분
var nt = UiKit . Title ( UiKit . FillLabel ( rib , it . nameKo , fs , owned ? UiKit . White : UiKit . Col ( "text_soft" , UiKit . InkSoft ) , TextAnchor . MiddleCenter , true , 22f ) , true ) ;
nt . horizontalOverflow = HorizontalWrapMode . Overflow ;
if ( owned )
{
// 진행 수치 = 최고 ★ 전이면 늘 조각 막대(레퍼런스 목록 「5/40」 · QA r3 U1) · 최고 ★ = 별 줄
if ( st . star < it . starMax )
{
int need , iron ;
TreasureRules . EnhanceCost ( S . Db , it , st . star + 1 , out need , out iron ) ;
var fill = UiKit . Bar ( "Shards" , t , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , shelfY + 2f ) , new Vector2 ( Mathf . Min ( 150f , size ) , 24f ) , UiKit . Slot , GradeColor ( it . grade ) ) ;
UiKit . SetBar ( fill , st . shards / ( float ) Mathf . Max ( 1 , need ) ) ;
UiKit . Title ( UiKit . FillLabel ( fill . parent , st . shards + "/" + need , 18 , UiKit . White ) , true ) ;
}
else if ( st . star > 0 ) UiKit . StarRow ( t , st . star , st . star , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , shelfY + 2f ) , 26f ) ;
}
}
/// <summary>칸 아래 = 별 줄 또는 조각 막대 + 이름 띠(예전 칸 · 진열대 칸 공통).</summary>
void CellFooter ( Transform t , TreasureDb . Item it , TreasureState st , bool owned , bool showShards )
{
2026-11-05 15:22:54 +00:00
if ( owned )
{
if ( showShards & & st . star < it . starMax )
{
int need , iron ;
TreasureRules . EnhanceCost ( S . Db , it , st . star + 1 , out need , out iron ) ;
2026-11-09 03:20:32 +00:00
var fill = UiKit . Bar ( "Shards" , t , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 76f ) , new Vector2 ( 150f , 26f ) , UiKit . Slot , GradeColor ( it . grade ) ) ;
2026-11-05 15:22:54 +00:00
UiKit . SetBar ( fill , st . shards / ( float ) Mathf . Max ( 1 , need ) ) ;
UiKit . FillLabel ( fill . parent , st . shards + "/" + need , 18 , UiKit . White ) ;
}
2026-11-09 03:20:32 +00:00
else if ( st . star > 0 ) UiKit . StarRow ( t , st . star , st . star , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 72f ) , 30f ) ;
2026-11-05 15:22:54 +00:00
}
2026-11-09 09:12:53 +00:00
// 이름 띠 = 칸 아래에 붙은 카드 띠(CardFrame01_Bottom · Fable §4-5) · 테마가 없으면 둥근 판
var band = UISkin . Has ( "card_bottom" )
? UiKit . Layer ( t , "Name" , "card_bottom" , UiKit . Col ( "card_bottom" , UiKit . Dark ) , 6f , 0f , 6f , 6f )
: UiKit . Skin ( UiKit . Rect ( "Name" , t , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 12f ) , new Vector2 ( 206f , 52f ) ) , "plate12" , UiKit . Col ( "gear_plate" , UiKit . Dark ) ) ;
if ( UISkin . Has ( "card_bottom" ) )
{
band . rectTransform . anchorMax = new Vector2 ( 1f , 0f ) ;
2026-11-12 14:29:32 +00:00
band . rectTransform . offsetMax = new Vector2 ( - 6f , NameBandH ) ;
2026-11-09 09:12:53 +00:00
}
var nt = UiKit . FillLabel ( band . transform , owned ? it . nameKo : "미보유" , 26 , owned ? UiKit . White : UiKit . Col ( "text_soft" , UiKit . InkSoft ) ) ;
2026-11-09 03:20:32 +00:00
nt . horizontalOverflow = HorizontalWrapMode . Overflow ;
2026-11-05 15:22:54 +00:00
}
// ---------- ① 보유 중 ----------
void BuildOwned ( RectTransform p )
{
var db = S . Db ;
int owned = 0 ;
foreach ( var it in db . Treasures . items )
if ( S . Save . inv . Treasure ( it . id ) ! = null ) owned + + ;
2026-11-09 09:12:53 +00:00
var cnt = UiKit . Title ( UiKit . Label ( p , "보유 " + owned + " / " + db . Treasures . items . Count , 36 , UiKit . Ink , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 28f , - 8f ) , new Vector2 ( 360f , 56f ) ) , UISkin . On ) ;
cnt . horizontalOverflow = HorizontalWrapMode . Overflow ;
2026-11-05 15:22:54 +00:00
var grades = db . Economy . TreasureGrades ;
for ( int i = 0 ; i < grades . Count ; i + + )
{
2026-11-09 09:12:53 +00:00
// 등급 범례 칩 = 서브탭 틀 소형(Round10 · Fable §4-5 「120× 56 통일」) · 등급 대표 색
var pos = new Vector2 ( - 24f - ( grades . Count - 1 - i ) * 126f , - 8f ) ;
var chip = UISkin . Has ( "subtab" )
? UiKit . Skin ( UiKit . Rect ( "G" + i , p , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , pos , new Vector2 ( 118f , 56f ) ) , "subtab" , GradeColor ( grades [ i ] . Key ) )
: UiKit . Box ( "G" + i , p , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , pos , new Vector2 ( 110f , 48f ) , GradeColor ( grades [ i ] . Key ) ) ;
UiKit . Title ( UiKit . FillLabel ( chip . transform , TreasureRules . GradeKo ( grades [ i ] . Key ) , 26 , UiKit . White ) , UISkin . On ) ;
2026-11-05 15:22:54 +00:00
}
ScrollRect sr ;
2026-11-13 07:49:11 +00:00
// 보물고 진열장 배경 = 진열장 안쪽 판에 맞춘 그리드(표 vault_grid · 위 = 진열장 왕관이 보이게) · 아니면 예전 그리드
var g = VaultRow ( "grid" ) ;
var grid = g ! = null
? UiKit . ScrollGrid ( "Grid" , p , g . x , g . x , g . y , 64f , new Vector2 ( g . w , g . h ) , new Vector2 ( g . gapMin , g . gapMax ) , Mathf . Max ( 1 , g . count ) , out sr )
: UiKit . ScrollGrid ( "Grid" , p , 24f , 24f , 76f , 64f , new Vector2 ( 240f , 290f ) , new Vector2 ( 16f , 16f ) , 4 , out sr ) ;
2026-11-05 15:22:54 +00:00
// 정렬 = 보유 먼저 · 등급 높은 순
var list = new List < TreasureDb . Item > ( db . Treasures . items ) ;
list . Sort ( ( a , b ) = >
{
bool oa = S . Save . inv . Treasure ( a . id ) ! = null , ob = S . Save . inv . Treasure ( b . id ) ! = null ;
if ( oa ! = ob ) return oa ? - 1 : 1 ;
return db . Economy . TreasureGradeOrder ( b . grade ) . CompareTo ( db . Economy . TreasureGradeOrder ( a . grade ) ) ;
} ) ;
GridCount = 0 ;
foreach ( var it in list )
{
TreasureCell ( grid , it , false ) ;
GridCount + + ;
}
var bonus = new List < string > ( ) ;
foreach ( var s in TreasureDb . BaseStats )
{
float v = TreasureRules . StatPct ( db , S . Save , s ) ;
if ( v > 0f ) bonus . Add ( TreasureRules . StatKo ( s ) + " +" + v . ToString ( "0.#" ) + "%" ) ;
}
2026-11-09 09:12:53 +00:00
string fx = bonus . Count > 0 ? "전체 효과 " + string . Join ( " · " , bonus . ToArray ( ) ) : "보물을 모으면 전 유닛에 상시 효과" ;
if ( UISkin . Has ( "chip" ) )
{
// 아래 효과 줄 = 정보 칩(§5-6)
var c = UiKit . Skin ( UiKit . Rect ( "Effects" , p , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 4f ) , new Vector2 ( 1032f , 54f ) ) , "chip" , UiKit . Col ( "chip_dark" , UiKit . Dark ) ) ;
var ft = UiKit . FillLabel ( c . transform , fx , 26 , UiKit . White , TextAnchor . MiddleCenter , false , 20f ) ;
ft . horizontalOverflow = HorizontalWrapMode . Overflow ;
}
else UiKit . Label ( p , fx , 26 , UiKit . InkSoft , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 8f ) , new Vector2 ( 1030f , 50f ) , false ) ;
2026-11-05 15:22:54 +00:00
}
public void Detail ( string id )
{
var db = S . Db ;
var d = S . Save ;
var it = db . Treasures . Get ( id ) ;
var st = d . inv . Treasure ( id ) ;
if ( it = = null | | st = = null ) return ;
2026-11-13 07:49:11 +00:00
// 선반 진열 무대 팝업(PD 2026-11-13 2차 「예시 이미지와 같이 선택된 유물의 정보 화면에 선반 컨셉을 강조」) — 무대 그림이 없으면 예전 글 팝업
var stageRow = DomesticScene . First ( TreasureScene . Scene ( "detail" ) , "stage" ) ;
var stageSp = stageRow ! = null ? UISkin . Icon ( stageRow . sprite ) : null ;
if ( stageSp ! = null )
{
DetailStage ( it , st , stageRow , stageSp ) ;
return ;
}
2026-11-05 15:22:54 +00:00
var sb = new System . Text . StringBuilder ( ) ;
sb . Append ( TreasureRules . GradeKo ( it . grade ) ) . Append ( " · ★" ) . Append ( st . star ) . Append ( "/" ) . Append ( it . starMax ) ;
var tiers = it . setId . Length > 0 ? db . TreasureSets . TiersOf ( it . setId ) : null ;
if ( tiers ! = null & & tiers . Count > 0 ) sb . Append ( " · 세트 「" ) . Append ( tiers [ 0 ] . nameKo ) . Append ( "」" ) ;
sb . Append ( "\n\n<b>기본 효과</b> " ) . Append ( TreasureRules . StatKo ( it . baseStat ) ) . Append ( " +" ) . Append ( TreasureRules . BasePct ( db , it , st . star ) . ToString ( "0.##" ) ) . Append ( "%" ) ;
int stacks = TreasureRules . CondStacks ( d , it ) ;
sb . Append ( "\n<b>조건 효과</b> " ) . Append ( TreasureRules . CondKo ( it . condKey ) ) . Append ( " " ) . Append ( it . condEvery ) . Append ( "회마다 +" ) . Append ( it . condPct . ToString ( "0.##" ) ) . Append ( "% (" )
. Append ( stacks ) . Append ( "/" ) . Append ( it . condMax ) . Append ( " · 누적 " ) . Append ( TreasureRules . Counter ( d , it . condKey ) ) . Append ( ")" ) ;
sb . Append ( "\n<size=28>= 지금 " ) . Append ( TreasureRules . StatKo ( it . baseStat ) ) . Append ( " +" ) . Append ( TreasureRules . Value ( db , d , it ) . ToString ( "0.##" ) ) . Append ( "%</size>" ) ;
string btn ;
var why = TreasureRules . CanEnhance ( db , d , id ) ;
if ( st . star < it . starMax )
{
int shards , iron ;
TreasureRules . EnhanceCost ( db , it , st . star + 1 , out shards , out iron ) ;
sb . Append ( "\n\n<b>강화 ★" ) . Append ( st . star + 1 ) . Append ( "</b> 조각 " ) . Append ( st . shards ) . Append ( "/" ) . Append ( shards ) . Append ( " · 운철 " ) . Append ( d . inv . iron ) . Append ( "/" ) . Append ( iron ) ;
btn = why = = null ? "강화" : why ;
}
else btn = "최고 ★" ;
Root . Modal ( it . nameKo , sb . ToString ( ) ,
new OutGameRoot . ModalButton ( btn , why = = null ? UiKit . Red : UiKit . Disabled , ( ) = >
{
var w = S . Do ( ( ) = > TreasureRules . Enhance ( S . Db , S . Save , id ) ) ;
if ( w = = null ) Detail ( id ) ;
else Root . Toast ( w ) ;
} , true ) ,
new OutGameRoot . ModalButton ( "닫기" , UiKit . Dark , null ) ) ;
}
2026-11-13 07:49:11 +00:00
/// <summary>정보 팝업이 무대 배치로 그려졌는가 · 무대 위 유물 아이콘(테스트).</summary>
public Image DetailRelic { get ; private set ; }
/// <summary>
/// 유물 정보 = 선반 진열 무대(PD 2026-11-13 2차 · 예시 = 제목 → 등급 리본 → 선반 위 받침에 놓인 유물 → 보유 속성 표(★ 현재 → 다음) → 효과 상자) —
/// 표 detail_stage(무대 그림 · 방석 윗면 · 아이콘 크기 · 리본 폭) · detail_ribbon. 버튼 = 강화(또는 사유·최고 ★) · 닫기.
/// </summary>
void DetailStage ( TreasureDb . Item it , TreasureState st , LobbyScene . Row stageRow , Sprite stageSp )
{
var db = S . Db ;
var d = S . Save ;
string id = it . id ;
var rows = TreasureScene . Scene ( "detail" ) ;
var ribRow = DomesticScene . First ( rows , "ribbon" ) ;
var ribSp = ribRow ! = null ? UISkin . Icon ( ribRow . sprite ) : null ;
bool maxed = st . star > = it . starMax ;
var why = TreasureRules . CanEnhance ( db , d , id ) ;
int needShards = 0 , needIron = 0 ;
if ( ! maxed ) TreasureRules . EnhanceCost ( db , it , st . star + 1 , out needShards , out needIron ) ;
string btn = maxed ? "최고 ★" : ( why = = null ? "강화 ★" + ( st . star + 1 ) : why ) ;
const float PanelH = 1320f ;
var content = Root . ModalCustom ( it . nameKo , PanelH ,
new OutGameRoot . ModalButton ( btn , why = = null & & ! maxed ? UiKit . Red : UiKit . Disabled , ( ) = >
{
var w = S . Do ( ( ) = > TreasureRules . Enhance ( S . Db , S . Save , id ) ) ;
if ( w = = null ) Detail ( id ) ;
else Root . Toast ( w ) ;
} , true ) ,
new OutGameRoot . ModalButton ( "닫기" , UiKit . Dark , null ) ) ;
const float W = 860f ; // 내용 칸 폭(팝업 940 − 좌우 40)
float stageW = stageRow . w > 1f ? stageRow . w : W ;
float k = stageW / Mathf . Max ( 1f , stageRow . h > 1f ? stageRow . h : stageSp . rect . width ) ;
float stageH = stageSp . rect . height * stageW / Mathf . Max ( 1f , stageSp . rect . width ) ;
// 무대 + 방석 위 유물
var stage = UiKit . At ( content , "Stage" , ( W - stageW ) * 0.5f , 26f , stageW , stageH ) ;
var si = stage . gameObject . AddComponent < Image > ( ) ;
si . sprite = stageSp ;
si . raycastTarget = false ;
UiKit . Border ( si , UiKit . Line , 3f ) ;
float iconSize = stageRow . min > 1f ? stageRow . min : 200f ;
string icon = IconId ( it ) ;
DetailRelic = icon ! = null ? UiKit . Icon ( stage , icon , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( stageRow . x * k , - stageRow . y * k - 6f ) , iconSize ) : null ;
if ( DetailRelic ! = null ) UiKit . DropShadow ( DetailRelic , 6f ) ;
// 등급 리본(무대 위 가운데 · 등급 색) — 등급 · ★
float rw = stageRow . max > 1f ? stageRow . max : 320f , rh = ribRow ! = null & & ribRow . h > 1f ? ribRow . h : 74f ;
var rib = UiKit . Rect ( "Grade" , content , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( W * 0.5f , - 26f ) , new Vector2 ( rw , rh ) ) ;
var rimg = rib . gameObject . AddComponent < Image > ( ) ;
rimg . sprite = ribSp ;
rimg . color = GradeColor ( it . grade ) ;
rimg . raycastTarget = false ;
if ( ribSp ! = null & & ribSp . border . sqrMagnitude > 0f ) rimg . type = Image . Type . Sliced ;
int rfs = ribRow ! = null & & ribRow . scale > 1f ? Mathf . RoundToInt ( ribRow . scale ) : 30 ;
UiKit . Title ( UiKit . FillLabel ( rib , TreasureRules . GradeKo ( it . grade ) + " ★" + st . star + "/" + it . starMax , rfs , UiKit . White , TextAnchor . MiddleCenter , true , 30f ) , true ) ;
// 보유 효과 제목(가는 줄 사이)
float y = 26f + stageH + 26f ;
UiKit . Label ( content , "보유 효과" , 30 , UiKit . Col ( "text_soft" , UiKit . InkSoft ) , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - y ) , new Vector2 ( 300f , 44f ) , false ) ;
UiKit . Paint ( UiKit . At ( content , "LineL" , 40f , y + 22f , 250f , 2f ) , UiKit . Col ( "text_soft" , UiKit . InkSoft ) , false ) ;
UiKit . Paint ( UiKit . At ( content , "LineR" , W - 290f , y + 22f , 250f , 2f ) , UiKit . Col ( "text_soft" , UiKit . InkSoft ) , false ) ;
y + = 58f ;
// 강화 단계 표(머리 = 어두운 띠 · ★ 지금 → 다음)
var headBand = UiKit . Paint ( UiKit . At ( content , "TableHead" , 0f , y , W , 60f ) , UiKit . Col ( "chip_dark" , UiKit . Dark ) , false ) ;
UiKit . Title ( UiKit . Label ( headBand . transform , "강화 단계" , 30 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 24f , 0f ) , new Vector2 ( 300f , 50f ) ) , true ) ;
UiKit . Title ( UiKit . Label ( headBand . transform , "★" + st . star , 30 , UiKit . Col ( "star" , UiKit . White ) , TextAnchor . MiddleCenter , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 520f , 0f ) , new Vector2 ( 160f , 50f ) ) , true ) ;
if ( ! maxed ) UiKit . Title ( UiKit . Label ( headBand . transform , "★" + ( st . star + 1 ) , 30 , UiKit . Col ( "star" , UiKit . White ) , TextAnchor . MiddleCenter , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 740f , 0f ) , new Vector2 ( 160f , 50f ) ) , true ) ;
y + = 60f ;
var rowBand = UiKit . Paint ( UiKit . At ( content , "TableRow" , 0f , y , W , 66f ) , UiKit . Col ( "chip" , UiKit . Paper ) , false ) ;
rowBand . color = new Color ( rowBand . color . r , rowBand . color . g , rowBand . color . b , 0.55f ) ;
UiKit . Label ( rowBand . transform , TreasureRules . StatKo ( it . baseStat ) , 30 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 24f , 0f ) , new Vector2 ( 360f , 50f ) ) ;
UiKit . Label ( rowBand . transform , "+" + TreasureRules . BasePct ( db , it , st . star ) . ToString ( "0.##" ) + "%" , 32 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 520f , 0f ) , new Vector2 ( 200f , 50f ) ) ;
if ( ! maxed )
{
UiKit . Label ( rowBand . transform , "≫" , 30 , UiKit . Col ( "text_soft" , UiKit . InkSoft ) , TextAnchor . MiddleCenter , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 630f , 0f ) , new Vector2 ( 60f , 50f ) ) ;
UiKit . Label ( rowBand . transform , "+" + TreasureRules . BasePct ( db , it , st . star + 1 ) . ToString ( "0.##" ) + "%" , 32 , UiKit . Ok , TextAnchor . MiddleCenter , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 740f , 0f ) , new Vector2 ( 200f , 50f ) ) ;
}
y + = 66f + 22f ;
// 조건 효과 상자(머리 띠 + 내용)
int stacks = TreasureRules . CondStacks ( d , it ) ;
var condHead = UiKit . Paint ( UiKit . At ( content , "CondHead" , 0f , y , W , 56f ) , UiKit . Col ( "chip_dark" , UiKit . Dark ) , false ) ;
UiKit . Title ( UiKit . Label ( condHead . transform , "조건 효과" , 28 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 24f , 0f ) , new Vector2 ( 400f , 50f ) ) , true ) ;
y + = 56f ;
var condBody = UiKit . Paint ( UiKit . At ( content , "CondBody" , 0f , y , W , 150f ) , UiKit . Col ( "chip" , UiKit . Paper ) , false ) ;
condBody . color = new Color ( condBody . color . r , condBody . color . g , condBody . color . b , 0.55f ) ;
var cb = new System . Text . StringBuilder ( ) ;
cb . Append ( "✦ " ) . Append ( TreasureRules . CondKo ( it . condKey ) ) . Append ( " " ) . Append ( it . condEvery ) . Append ( "회마다 " ) . Append ( TreasureRules . StatKo ( it . baseStat ) ) . Append ( " +" ) . Append ( it . condPct . ToString ( "0.##" ) ) . Append ( "%" )
. Append ( " (" ) . Append ( stacks ) . Append ( "/" ) . Append ( it . condMax ) . Append ( " · 누적 " ) . Append ( TreasureRules . Counter ( d , it . condKey ) ) . Append ( ")" ) ;
cb . Append ( "\n✦ 지금 " ) . Append ( TreasureRules . StatKo ( it . baseStat ) ) . Append ( " <color=#" ) . Append ( ColorUtility . ToHtmlStringRGB ( UiKit . Ok ) ) . Append ( ">+" ) . Append ( TreasureRules . Value ( db , d , it ) . ToString ( "0.##" ) ) . Append ( "%</color>" ) ;
UiKit . FillLabel ( condBody . transform , cb . ToString ( ) , 28 , UiKit . White , TextAnchor . MiddleLeft , false , 24f ) ;
y + = 150f + 18f ;
// 세트 · 강화 재료
var tiers = it . setId . Length > 0 ? db . TreasureSets . TiersOf ( it . setId ) : null ;
var foot = new List < string > ( ) ;
if ( tiers ! = null & & tiers . Count > 0 ) foot . Add ( "세트 「" + tiers [ 0 ] . nameKo + "」" ) ;
if ( ! maxed ) foot . Add ( "조각 " + st . shards + "/" + needShards + " · 운철 " + d . inv . iron + "/" + needIron ) ;
if ( foot . Count > 0 )
UiKit . Label ( content , string . Join ( " " , foot . ToArray ( ) ) , 28 , UiKit . Col ( "text_soft" , UiKit . InkSoft ) , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - y ) , new Vector2 ( W , 44f ) , false ) ;
}
2026-11-05 15:22:54 +00:00
// ---------- ② 세트 ----------
void BuildSets ( RectTransform p )
{
var db = S . Db ;
var d = S . Save ;
ScrollRect sr ;
2026-11-13 07:49:11 +00:00
// 보물고 진열장 배경 = 묶음 폭을 진열장 안쪽 판에 맞춤(표 vault_setgrid) · 아니면 예전 1032
var sg = VaultRow ( "setgrid" ) ;
int perRow = sg ! = null & & sg . count > 0 ? sg . count : 5 ;
float mw = sg ! = null & & sg . w > 1f ? sg . w : 196f , mh = sg ! = null & & sg . h > 1f ? sg . h : 290f , mgap = sg ! = null & & sg . gapMin > 0f ? sg . gapMin : 12f ;
float bw = sg ! = null ? perRow * mw + ( perRow - 1 ) * mgap : 1032f ;
float pad = sg ! = null ? ( OutGameRoot . Reference . x - bw ) * 0.5f : 24f ;
var list = UiKit . ScrollGrid ( "Sets" , p , pad , pad , sg ! = null ? sg . y : 6f , 12f , new Vector2 ( bw , 640f ) , new Vector2 ( 0f , 24f ) , 1 , out sr ) ;
2026-11-05 15:22:54 +00:00
SetCount = 0 ;
foreach ( var setId in db . TreasureSets . SetIds )
{
var tiers = db . TreasureSets . TiersOf ( setId ) ;
if ( tiers . Count = = 0 ) continue ;
var head = tiers [ 0 ] ;
var block = UiKit . Cell ( list , "Set_" + setId , new Color ( 0f , 0f , 0f , 0f ) , null ) ;
block . raycastTarget = false ;
2026-11-09 03:20:32 +00:00
var band = UiKit . Box ( "Band" , block . transform , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , Vector2 . zero , new Vector2 ( 700f , 70f ) , UiKit . Dark ) ;
2026-11-05 15:22:54 +00:00
UiKit . FillLabel ( band . transform , head . nameKo , 40 , UiKit . White ) ;
int owned = TreasureRules . OwnedIn ( d , head ) ;
2026-11-13 07:49:11 +00:00
// 보물고(붉은 벽) 위 = 흰 글자 + 외곽선(보유 중 「보유 n / m」 과 같은 모양 · 붉은 글자는 벽에 묻힘 · QA r1 M2)
bool vaultBg = ShownScene = = "vault" ;
var ownedLbl = UiKit . Label ( block . transform , "보유 " + owned + "/" + head . members . Count , 30 , owned = = head . members . Count | | vaultBg ? UiKit . Ink : UiKit . Red , TextAnchor . MiddleRight , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( vaultBg ? - 14f : 0f , - 10f ) , new Vector2 ( 200f , 50f ) ) ;
if ( vaultBg ) UiKit . Title ( ownedLbl , true ) ; // 진열장 틀에서 14 px 띄움(QA r3 M8)
var row = UiKit . Rect ( "Members" , block . transform , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 84f ) , new Vector2 ( bw , mh ) ) ;
2026-11-05 15:22:54 +00:00
var hl = row . gameObject . AddComponent < HorizontalLayoutGroup > ( ) ;
2026-11-13 07:49:11 +00:00
hl . spacing = mgap ;
2026-11-05 15:22:54 +00:00
hl . childAlignment = TextAnchor . UpperCenter ;
hl . childControlWidth = hl . childControlHeight = false ;
foreach ( var m in head . members )
{
var it = db . Treasures . Get ( m ) ;
if ( it = = null ) continue ;
2026-11-13 07:49:11 +00:00
var holder = UiKit . Rect ( "M" , row , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , new Vector2 ( mw , mh ) ) ;
2026-11-05 15:22:54 +00:00
TreasureCell ( holder , it , true ) ;
var c = holder . GetChild ( 0 ) as RectTransform ;
c . anchorMin = Vector2 . zero ;
c . anchorMax = Vector2 . one ;
c . offsetMin = c . offsetMax = Vector2 . zero ;
}
2026-11-13 07:49:11 +00:00
var fx = UiKit . Box ( "Effects" , block . transform , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , Vector2 . zero , new Vector2 ( bw , 250f ) , UiKit . Paper ) ;
2026-11-05 15:22:54 +00:00
UiKit . Border ( fx , UiKit . Line , 2f ) ;
var best = TreasureRules . SetTier ( db , d , setId ) ;
var sb = new System . Text . StringBuilder ( "<b>기본 효과</b> <size=26>(가장 높은 달성 단계 1개)</size>" ) ;
2026-11-09 03:20:32 +00:00
string okHex = "#" + ColorUtility . ToHtmlStringRGB ( UiKit . Ok ) , offHex = "#" + ColorUtility . ToHtmlStringRGB ( UiKit . InkSoft ) ;
2026-11-05 15:22:54 +00:00
foreach ( var t in tiers )
{
bool ok = TreasureRules . TierReached ( d , t ) ;
string label = t . stars = = 0 ? "[세트]" : "[" + t . stars + "성]" ;
string stat = TreasureRules . StatKo ( t . effectKey . Replace ( "_pct" , "" ) ) ;
2026-11-09 03:20:32 +00:00
sb . Append ( "\n" ) . Append ( ok ? ( t = = best ? "<color=" + okHex + "><b>" : "<color=" + okHex + ">" ) : "<color=" + offHex + ">" ) . Append ( label ) . Append ( " " ) . Append ( stat ) . Append ( " +" ) . Append ( t . pct . ToString ( "0.#" ) ) . Append ( "%" )
2026-11-05 15:22:54 +00:00
. Append ( ok ? ( t = = best ? " ✔ 적용</b>" : " 달성" ) : " 미달성" ) . Append ( "</color>" ) ;
}
UiKit . FillLabel ( fx . transform , sb . ToString ( ) , 30 , UiKit . Ink , TextAnchor . MiddleLeft , false , 24f ) ;
SetCount + + ;
}
}
// ---------- ③ 상자 ----------
void BuildChests ( RectTransform p )
{
var db = S . Db ;
var d = S . Save ;
var e = db . Economy ;
var goalBox = UiKit . TopBand ( "Points" , p , 6f , 150f , 24f , 24f ) ;
var gimg = UiKit . Paint ( goalBox , UiKit . Paper ) ;
UiKit . Border ( gimg , UiKit . Line , 3f ) ;
2026-11-06 14:20:57 +00:00
if ( UiKit . Icon ( goalBox , "chest_point" , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 16f , 0f ) , 100f ) = = null )
UiKit . FillLabel ( UiKit . Dot ( "P" , goalBox , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 20f , 0f ) , 90f , UiKit . Coin ) . transform , "P" , 44 , UiKit . Ink ) ;
2026-11-05 15:22:54 +00:00
var reward = db . Chests . Get ( e . ChestPointReward ) ;
UiKit . Label ( goalBox , ( e . ChestPointGoal - d . inv . chestPoints ) + "포인트 더 모으면 " + ( reward ! = null ? reward . nameKo : "" ) , 34 , UiKit . Ink , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 130f , - 12f ) , new Vector2 ( 800f , 54f ) ) ;
2026-11-09 03:20:32 +00:00
var fill = UiKit . Bar ( "Bar" , goalBox , new Vector2 ( 0f , 0f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 130f , 52f ) , new Vector2 ( 700f , 30f ) , UiKit . Slot , UiKit . Coin ) ;
2026-11-05 15:22:54 +00:00
UiKit . SetBar ( fill , d . inv . chestPoints / ( float ) Mathf . Max ( 1 , e . ChestPointGoal ) ) ;
UiKit . Label ( goalBox , "상자 포인트 " + d . inv . chestPoints + " / " + e . ChestPointGoal , 26 , UiKit . InkSoft , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 130f , 8f ) , new Vector2 ( 700f , 40f ) , false ) ;
var c = db . Chests . Get ( SelectedChest ) ;
if ( c = = null ) return ;
int count = TreasureRules . ChestCount ( d , c . id ) ;
var banner = UiKit . Box ( "Banner" , p , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 180f ) , new Vector2 ( 560f , 470f ) , ChestColor ( c . id ) ) ;
UiKit . Border ( banner , new Color ( 0.25f , 0.2f , 0.18f ) , 4f ) ;
2026-11-06 14:20:57 +00:00
UiKit . Title ( UiKit . Label ( banner . transform , c . nameKo , 60 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 40f ) , new Vector2 ( 520f , 90f ) ) , true ) ;
UiKit . Icon ( banner . transform , "chest_" + c . id , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 10f , - 10f ) , 150f ) ;
2026-11-05 15:22:54 +00:00
int line = 0 ;
for ( int i = 0 ; i < ChestDb . ProbGrades . Length ; i + + )
{
if ( c . p [ i ] < = 0f ) continue ;
var chip = UiKit . Box ( "P" + i , banner . transform , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 150f - line * 62f ) , new Vector2 ( 360f , 54f ) , GradeColor ( ChestDb . ProbGrades [ i ] ) ) ;
UiKit . FillLabel ( chip . transform , TreasureRules . GradeKo ( ChestDb . ProbGrades [ i ] ) + " " + ( c . p [ i ] * 100f ) . ToString ( "0.#" ) + " %" , 30 , UiKit . White ) ;
line + + ;
}
var extra = new List < string > ( ) ;
if ( c . goldMax > 0 ) extra . Add ( "골드 " + c . goldMin + "~" + c . goldMax ) ;
if ( c . gemMax > 0 ) extra . Add ( "보옥 " + c . gemMin + "~" + c . gemMax ) ;
if ( c . ironMax > 0 ) extra . Add ( "운철 " + c . ironMin + "~" + c . ironMax ) ;
extra . Add ( "포인트 +" + c . points ) ;
UiKit . Label ( banner . transform , string . Join ( " · " , extra . ToArray ( ) ) , 26 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 18f ) , new Vector2 ( 540f , 50f ) , false ) ;
UiKit . Btn ( "Prev" , p , "◁" , 70 , new Color ( 0f , 0f , 0f , 0f ) , UiKit . Ink , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 40f , - 360f ) , new Vector2 ( 120f , 140f ) , ( ) = > Step ( - 1 ) ) ;
UiKit . Btn ( "Next" , p , "▷" , 70 , new Color ( 0f , 0f , 0f , 0f ) , UiKit . Ink , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( - 40f , - 360f ) , new Vector2 ( 120f , 140f ) , ( ) = > Step ( 1 ) ) ;
var open = UiKit . Btn ( "Open" , p , count > 0 ? "열기\n<size=28>보유 " + count + "</size>" : "열기\n<size=28>상자 없음</size>" , 46 , count > 0 ? UiKit . Red : UiKit . Disabled , UiKit . White ,
new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 680f ) , new Vector2 ( 480f , 140f ) , ( ) = > DoOpen ( SelectedChest ) ) ;
open . interactable = count > 0 ;
UiKit . Label ( p , "상자는 이 탭에서만 엽니다 · 중복 = 조각 · 천기 = 적장 토벌 전용" , 26 , UiKit . InkSoft , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 836f ) , new Vector2 ( 1000f , 44f ) , false ) ;
int n = db . Chests . chests . Count ;
float w = ( 1032f - ( n - 1 ) * 12f ) / n ;
for ( int i = 0 ; i < n ; i + + )
{
var ch = db . Chests . chests [ i ] ;
int k = i ;
2026-11-09 03:20:32 +00:00
var b = UiKit . Btn ( "Chest_" + ch . id , p , "" , 28 , i = = chestIdx ? UiKit . Red : UiKit . Paper , UiKit . Ink , new Vector2 ( 0f , 0f ) , new Vector2 ( 0f , 0f ) , new Vector2 ( 24f + i * ( w + 12f ) , 24f ) , new Vector2 ( w , 220f ) , ( ) = >
2026-11-05 15:22:54 +00:00
{
chestIdx = k ;
2026-11-09 09:23:18 +00:00
chestPicked = true ;
2026-11-05 15:22:54 +00:00
Refresh ( ) ;
} ) ;
UiKit . Border ( b . GetComponent < Image > ( ) , i = = chestIdx ? UiKit . Coin : UiKit . Line , i = = chestIdx ? 5f : 2f ) ;
var box = UiKit . Box ( "Icon" , b . transform , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 20f ) , new Vector2 ( w * 0.7f , 90f ) , ChestColor ( ch . id ) ) ;
box . raycastTarget = false ;
2026-11-06 14:20:57 +00:00
UiKit . Icon ( box . transform , "chest_" + ch . id , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , 4f ) , 96f ) ;
2026-11-05 15:22:54 +00:00
UiKit . Label ( b . transform , ch . nameKo + "\n<size=34>× " + TreasureRules . ChestCount ( d , ch . id ) + "</size>" , 26 , UiKit . Ink , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 14f ) , new Vector2 ( w , 100f ) ) ;
}
}
2026-11-09 09:12:53 +00:00
/// <summary>
/// 상자 탭(Fable 제안서 §4-5 · #1104) — 포인트 띠(BannerFrame05 · 가는 주황 바 · 「85 / 120」 · 오른쪽 보상 상자) →
/// 상자 카드(CardFrame03 상자 색 + 안쪽 + 광택 · 상자 아이콘 300 + 뒤 금빛 · 등급 칩 + % 한 줄 · 내용물 줄) · 좌우 화살표 →
/// 「열기」 Primary 노랑 + 보유 칩(없으면 회색) → 아래 상자 종류 행(ItemFrame 5층 상자 색 + 수량 칩 · 선택 = FocusLine 깜빡).
/// </summary>
void BuildChestsKit ( RectTransform p )
{
var db = S . Db ;
var d = S . Save ;
var soft = UiKit . Col ( "text_soft" , UiKit . InkSoft ) ;
2026-11-13 07:49:11 +00:00
PointsBand ( p ) ;
AutoPickChest ( ) ;
var c = db . Chests . Get ( SelectedChest ) ;
if ( c = = null ) return ;
int count = TreasureRules . ChestCount ( d , c . id ) ;
BuildChestsKitBody ( p , c , count , soft ) ;
ChestRow ( p , soft ) ;
}
2026-11-09 09:12:53 +00:00
2026-11-13 07:49:11 +00:00
/// <summary>
/// 상자 탭 = 금고(PD 2026-11-13 「상자 메뉴의 배경은 화려한 금고(주변에 금화가 쌓인) 속 올려진 상자를 여는 형태로 재구성」) —
/// 포인트 띠 → 금고 그림 받침대 방석 위에 상자(표 chest_spot · 뒤 금빛 맥동) · 위 이름 · 아래 확률 칩 · 내용물 줄 · 좌우 화살표 · 열기(누르면 상자가 흔들린 뒤 결과) · 아래 상자 종류 행.
/// 자리 = 표 Resources/UI/treasure_scene.csv(그림 px · ArtToPage 로 페이지 좌표).
/// </summary>
void BuildChestsVault ( RectTransform p , Scene s )
{
var db = S . Db ;
var d = S . Save ;
PointsBand ( p ) ;
AutoPickChest ( ) ;
var c = db . Chests . Get ( SelectedChest ) ;
if ( c = = null ) return ;
int count = TreasureRules . ChestCount ( d , c . id ) ;
ChestOnPedestal = true ;
var spot = DomesticScene . First ( s . rows , "spot" ) ;
float sx = spot ! = null ? spot . x : s . artW * 0.5f , sy = spot ! = null ? spot . y : s . artH * 0.33f ;
float size = spot ! = null & & spot . w > 1f ? spot . w : 360f ;
var at = ArtToPage ( s , sx , sy ) ;
System . Func < string , float , float > rowY = ( kind , def ) = >
{
var r = DomesticScene . First ( s . rows , kind ) ;
return ArtToPage ( s , sx , r ! = null ? r . y : def ) . y ;
} ;
// 뒤 금빛 맥동 → 상자(받침대 방석 위)
if ( UISkin . Has ( "glow_circle" ) )
{
var glow = UiKit . Skin ( UiKit . Rect ( "Glow" , p , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( at . x , - at . y ) , new Vector2 ( size * 1.7f , size * 1.7f ) ) , "glow_circle" , UiKit . Col ( "chest_glow" , Color . clear ) ) ;
var pulse = glow . gameObject . AddComponent < UiPulse > ( ) ;
pulse . min = 0.7f ;
pulse . period = 1.6f ;
}
openChestIcon = UiKit . Icon ( p , "chest_" + c . id , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( at . x , - at . y ) , size ) ;
if ( openChestIcon ! = null ) UiKit . DropShadow ( openChestIcon , 8f ) ;
// 이름 · 확률 칩 · 내용물 줄(금 바닥 위 = 외곽선 글자)
var name = UiKit . Title ( UiKit . Label ( p , c . nameKo , 56 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( at . x , - rowY ( "name" , sy - 210f ) ) , new Vector2 ( 700f , 80f ) ) , true ) ;
name . horizontalOverflow = HorizontalWrapMode . Overflow ;
var probs = new List < int > ( ) ;
for ( int i = 0 ; i < ChestDb . ProbGrades . Length ; i + + )
if ( c . p [ i ] > 0f ) probs . Add ( i ) ;
const float Pw = 178f , Pg = 14f ;
float py = rowY ( "probs" , sy + 440f ) ;
float px = at . x - ( probs . Count * Pw + ( probs . Count - 1 ) * Pg ) * 0.5f ;
for ( int k = 0 ; k < probs . Count ; k + + )
{
int i = probs [ k ] ;
string g = ChestDb . ProbGrades [ i ] ;
var chip = UiKit . Skin ( UiKit . At ( p , "P" + i , px + k * ( Pw + Pg ) , py - 30f , Pw , 60f ) , "para_orange" , GradeColor ( g ) ) ;
var ct = UiKit . Title ( UiKit . FillLabel ( chip . transform , TreasureRules . GradeKo ( g ) + " " + ( c . p [ i ] * 100f ) . ToString ( "0.#" ) + "%" , 30 , UiKit . White ) , true ) ;
ct . horizontalOverflow = HorizontalWrapMode . Overflow ;
}
var extra = new List < string > ( ) ;
if ( c . goldMax > 0 ) extra . Add ( "골드 " + c . goldMin + "~" + c . goldMax ) ;
if ( c . gemMax > 0 ) extra . Add ( "보옥 " + c . gemMin + "~" + c . gemMax ) ;
if ( c . ironMax > 0 ) extra . Add ( "운철 " + c . ironMin + "~" + c . ironMax ) ;
extra . Add ( "포인트 +" + c . points ) ;
var eb = UiKit . Skin ( UiKit . Rect ( "ExtraBand" , p , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( at . x , - rowY ( "extra" , sy + 500f ) ) , new Vector2 ( 720f , 52f ) ) , "chip" , UiKit . Col ( "chip_dark" , UiKit . Dark ) ) ;
var et = UiKit . Title ( UiKit . FillLabel ( eb . transform , string . Join ( " · " , extra . ToArray ( ) ) , 26 , UiKit . White , TextAnchor . MiddleCenter , true , 16f ) , true ) ;
et . horizontalOverflow = HorizontalWrapMode . Overflow ;
// 좌우 화살표(상자 높이)
var ar = DomesticScene . First ( s . rows , "arrow" ) ;
float ax = ar ! = null ? ar . x : 30f ;
float ay = rowY ( "arrow" , sy ) ;
Arrow ( p , "Prev" , "arrow_prev" , ax , ay - 70f , ( ) = > Step ( - 1 ) ) ;
Arrow ( p , "Next" , "arrow_next" , 1080f - ax - 100f , ay - 70f , ( ) = > Step ( 1 ) ) ;
// 열기(받침대 위 상자가 흔들린 뒤 결과) · 안내
float oy = rowY ( "open" , sy + 540f ) ;
var open = UiKit . SkinBtn ( p , "Open" , count > 0 ? "btn_l_yellow" : "btn_gray" , UiKit . White , count > 0 ? "열기" : "상자 없음" , 46 ,
count > 0 ? UiKit . Col ( "text_on_yellow" , UiKit . Ink ) : UiKit . White , ( 1080f - 460f ) * 0.5f , oy , 460f , 116f , ( ) = > OpenWithShake ( SelectedChest ) ) ;
open . interactable = count > 0 ;
if ( count > 0 ) UiKit . BtnCost ( open , "chest_" + c . id , "x" + count , 56f , 34 ) ;
var note = UiKit . Title ( UiKit . Label ( p , "상자는 이 탭에서만 엽니다 · 중복 = 조각 · 천기 = 적장 토벌 전용" , 26 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - rowY ( "note" , sy + 690f ) ) , new Vector2 ( 1000f , 44f ) , false ) , true ) ;
note . horizontalOverflow = HorizontalWrapMode . Overflow ;
ChestRow ( p , UiKit . White ) ;
}
/// <summary>열기 버튼과 같은 동작(받침대 배치면 흔든 뒤 열기 · 아니면 바로) — 테스트·캡처용 공개 진입점.</summary>
public void OpenAnimated ( string id )
{
OpenWithShake ( id ) ;
}
/// <summary>받침대 위 상자를 흔든 뒤(0.4 초 · 실제 시간) 연다 — 상자가 없으면 바로 DoOpen(토스트).</summary>
void OpenWithShake ( string id )
{
if ( openChestIcon = = null | | Root = = null | | ! Root . isActiveAndEnabled )
{
DoOpen ( id ) ;
return ;
}
Root . StartCoroutine ( ShakeThenOpen ( openChestIcon . rectTransform , id ) ) ;
}
System . Collections . IEnumerator ShakeThenOpen ( RectTransform rt , string id )
{
const float Dur = 0.4f ;
float t = 0f ;
while ( t < Dur & & rt ! = null )
{
t + = Time . unscaledDeltaTime ;
float k = t / Dur ;
rt . localRotation = Quaternion . Euler ( 0f , 0f , Mathf . Sin ( k * Mathf . PI * 8f ) * 7f * ( 1f - k ) ) ;
rt . localScale = Vector3 . one * ( 1f + 0.12f * Mathf . Sin ( k * Mathf . PI ) ) ;
yield return null ;
}
if ( rt ! = null )
{
rt . localRotation = Quaternion . identity ;
rt . localScale = Vector3 . one ;
}
DoOpen ( id ) ;
}
/// <summary>상자 포인트 띠(맨 위 · 「n포인트 더 모으면 명장 상자」 · 바 · 「85 / 120」 · 오른쪽 보상 상자).</summary>
void PointsBand ( RectTransform p )
{
var db = S . Db ;
var d = S . Save ;
var e = db . Economy ;
2026-11-09 09:12:53 +00:00
var band = UiKit . Skin ( UiKit . At ( p , "Points" , 24f , 6f , 1032f , 92f ) , "banner_point" , UiKit . Col ( "banner_point" , UiKit . White ) ) . transform ;
var pi = UiKit . Icon ( band , "chest_point" , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 46f , 2f ) , 76f ) ;
if ( pi ! = null ) UiKit . DropShadow ( pi , 3f ) ;
var reward = db . Chests . Get ( e . ChestPointReward ) ;
UiKit . Label ( band , Mathf . Max ( 0 , e . ChestPointGoal - d . inv . chestPoints ) + "포인트 더 모으면 " + ( reward ! = null ? reward . nameKo : "" ) , 28 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 112f , - 6f ) , new Vector2 ( 640f , 44f ) ) ;
var bg = UiKit . Skin ( UiKit . At ( band , "Bar" , 112f , 50f , 600f , 30f ) , "bar4_bg" , UiKit . White ) ;
var fill = UiKit . Skin ( UiKit . At ( bg . transform , "Fill" , 4f , 4f , 592f , 20f ) , "bar4_fill" , UiKit . White ) . rectTransform ;
float v = Mathf . Clamp01 ( d . inv . chestPoints / ( float ) Mathf . Max ( 1 , e . ChestPointGoal ) ) ;
fill . sizeDelta = new Vector2 ( Mathf . Max ( 16f , 592f * v ) , 20f ) ;
fill . gameObject . SetActive ( v > 0.001f ) ;
var pt = UiKit . Title ( UiKit . Label ( band , d . inv . chestPoints + " / " + e . ChestPointGoal , 30 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 730f , - 38f ) , new Vector2 ( 180f , 50f ) ) , true ) ;
pt . horizontalOverflow = HorizontalWrapMode . Overflow ;
if ( reward ! = null )
{
var ri = UiKit . Icon ( band , "chest_" + reward . id , new Vector2 ( 1f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( - 62f , 4f ) , 80f ) ;
if ( ri ! = null ) UiKit . DropShadow ( ri , 3f ) ;
}
2026-11-13 07:49:11 +00:00
}
2026-11-09 09:12:53 +00:00
2026-11-13 07:49:11 +00:00
/// <summary>처음 연 상자 탭에서 고른 상자가 0개면 가진 상자 중 첫 번째로.</summary>
void AutoPickChest ( )
{
var db = S . Db ;
var d = S . Save ;
2026-11-09 09:23:18 +00:00
if ( ! chestPicked & & TreasureRules . ChestCount ( d , SelectedChest ) = = 0 )
for ( int i = 0 ; i < db . Chests . chests . Count ; i + + )
if ( TreasureRules . ChestCount ( d , db . Chests . chests [ i ] . id ) > 0 )
{
chestIdx = i ;
break ;
}
2026-11-13 07:49:11 +00:00
}
2026-11-09 09:12:53 +00:00
2026-11-13 07:49:11 +00:00
/// <summary>예전 상자 탭 몸통(무대 문양 + 카드 + 화살표 + 열기) — 금고 배경 그림이 없을 때.</summary>
void BuildChestsKitBody ( RectTransform p , ChestDb . Chest c , int count , Color soft )
{
2026-11-09 09:23:18 +00:00
// 무대 = 카드 뒤 문양 흰 5 % + 바닥 그라데이션(검수2 결함 4)
if ( UISkin . Has ( "bg_pattern" ) )
{
var stage = UiKit . At ( p , "Stage" , 0f , 108f , 1080f , 1000f ) ;
var pat = UiKit . Skin ( stage , "bg_pattern" , UiKit . Col ( "stage_pattern" , Color . clear ) ) ;
pat . type = Image . Type . Tiled ;
if ( UISkin . Has ( "scroll_fade" ) )
{
var g = UiKit . Skin ( UiKit . At ( stage , "Ground" , 0f , 1000f - 160f , 1080f , 160f ) , "scroll_fade" , UiKit . Col ( "panel_ground" , Color . clear ) ) ;
g . rectTransform . localScale = new Vector3 ( 1f , - 1f , 1f ) ;
g . rectTransform . pivot = new Vector2 ( 0f , 0f ) ;
}
}
2026-11-09 09:12:53 +00:00
// 상자 카드
2026-11-09 09:23:18 +00:00
const float Cw = 680f , Ch = 760f , Cy = 120f ;
2026-11-09 09:12:53 +00:00
var card = UiKit . At ( p , "Card" , ( 1080f - Cw ) * 0.5f , Cy , Cw , Ch ) ;
UiKit . Skin ( card , "card3_bg" , ChestColor ( c . id ) ) ;
UiKit . Layer ( card , "Inner" , "card3_inner" , UiKit . Col ( "card3_inner" , Color . clear ) , 18f , 104f , 18f , 18f ) ;
UiKit . Layer ( card , "Light" , "card3_light" , UiKit . Col ( "card3_light" , Color . clear ) , 6f , 6f , 6f , 10f ) ;
var name = UiKit . Title ( UiKit . Label ( card , c . nameKo , 56 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 16f ) , new Vector2 ( Cw - 40f , 80f ) ) , true ) ;
name . horizontalOverflow = HorizontalWrapMode . Overflow ;
if ( UISkin . Has ( "glow_circle" ) )
{
2026-11-09 09:23:18 +00:00
var glow = UiKit . Skin ( UiKit . Rect ( "Glow" , card , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - 320f ) , new Vector2 ( 600f , 600f ) ) , "glow_circle" , UiKit . Col ( "chest_glow" , Color . clear ) ) ;
2026-11-09 09:12:53 +00:00
var pulse = glow . gameObject . AddComponent < UiPulse > ( ) ;
pulse . min = 0.7f ;
pulse . period = 1.6f ;
}
2026-11-09 09:23:18 +00:00
var ci = UiKit . Icon ( card , "chest_" + c . id , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , - 320f ) , 320f ) ;
2026-11-09 09:12:53 +00:00
if ( ci ! = null ) UiKit . DropShadow ( ci , 8f ) ;
var probs = new List < int > ( ) ;
for ( int i = 0 ; i < ChestDb . ProbGrades . Length ; i + + )
if ( c . p [ i ] > 0f ) probs . Add ( i ) ;
const float Pw = 178f , Pg = 14f ;
float px = ( Cw - probs . Count * Pw - ( probs . Count - 1 ) * Pg ) * 0.5f ;
for ( int k = 0 ; k < probs . Count ; k + + )
{
int i = probs [ k ] ;
string g = ChestDb . ProbGrades [ i ] ;
2026-11-09 09:23:18 +00:00
var chip = UiKit . Skin ( UiKit . At ( card , "P" + i , px + k * ( Pw + Pg ) , 528f , Pw , 60f ) , "para_orange" , GradeColor ( g ) ) ;
var ct = UiKit . Title ( UiKit . FillLabel ( chip . transform , TreasureRules . GradeKo ( g ) + " " + ( c . p [ i ] * 100f ) . ToString ( "0.#" ) + "%" , 30 , UiKit . White ) , true ) ;
2026-11-09 09:12:53 +00:00
ct . horizontalOverflow = HorizontalWrapMode . Overflow ;
}
var extra = new List < string > ( ) ;
if ( c . goldMax > 0 ) extra . Add ( "골드 " + c . goldMin + "~" + c . goldMax ) ;
if ( c . gemMax > 0 ) extra . Add ( "보옥 " + c . gemMin + "~" + c . gemMax ) ;
if ( c . ironMax > 0 ) extra . Add ( "운철 " + c . ironMin + "~" + c . ironMax ) ;
extra . Add ( "포인트 +" + c . points ) ;
2026-11-09 09:23:18 +00:00
// 내용물 줄 = 카드 아래 어두운 띠 위 흰 글자(검수2 결함 10)
var eb = UiKit . Layer ( card , "ExtraBand" , "card_bottom" , UiKit . Col ( "card_bottom" , UiKit . Dark ) , 6f , 0f , 6f , 6f ) ;
eb . rectTransform . anchorMax = new Vector2 ( 1f , 0f ) ;
eb . rectTransform . offsetMax = new Vector2 ( - 6f , 76f ) ;
var et = UiKit . Title ( UiKit . FillLabel ( eb . transform , string . Join ( " · " , extra . ToArray ( ) ) , 26 , UiKit . White , TextAnchor . MiddleCenter , true , 16f ) , true ) ;
et . horizontalOverflow = HorizontalWrapMode . Overflow ;
2026-11-09 09:12:53 +00:00
// 좌우 화살표(카드 가운데 높이)
Arrow ( p , "Prev" , "arrow_prev" , 30f , Cy + Ch * 0.5f - 70f , ( ) = > Step ( - 1 ) ) ;
Arrow ( p , "Next" , "arrow_next" , 1080f - 30f - 100f , Cy + Ch * 0.5f - 70f , ( ) = > Step ( 1 ) ) ;
// 열기
var open = UiKit . SkinBtn ( p , "Open" , count > 0 ? "btn_l_yellow" : "btn_gray" , UiKit . White , count > 0 ? "열기" : "상자 없음" , 46 ,
count > 0 ? UiKit . Col ( "text_on_yellow" , UiKit . Ink ) : UiKit . White , ( 1080f - 460f ) * 0.5f , Cy + Ch + 26f , 460f , 116f , ( ) = > DoOpen ( SelectedChest ) ) ;
open . interactable = count > 0 ;
if ( count > 0 ) UiKit . BtnCost ( open , "chest_" + c . id , "x" + count , 56f , 34 ) ;
UiKit . Label ( p , "상자는 이 탭에서만 엽니다 · 중복 = 조각 · 천기 = 적장 토벌 전용" , 26 , soft , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - ( Cy + Ch + 156f ) ) , new Vector2 ( 1000f , 44f ) , false ) ;
2026-11-13 07:49:11 +00:00
}
2026-11-09 09:12:53 +00:00
2026-11-13 07:49:11 +00:00
/// <summary>상자 종류 행(아래 · ItemFrame 5층 상자 색 + 수량 칩 · 선택 = FocusLine 깜빡) — 예전 배치·금고 배치 공통.</summary>
void ChestRow ( RectTransform p , Color soft )
{
var db = S . Db ;
var d = S . Save ;
2026-11-09 09:12:53 +00:00
int n = db . Chests . chests . Count ;
const float Tw = 150f ;
float gap = n > 1 ? Mathf . Min ( 64f , ( 1032f - n * Tw ) / ( n - 1 ) ) : 0f ;
float tx0 = ( 1080f - n * Tw - ( n - 1 ) * gap ) * 0.5f ;
float ty = OutGameRoot . BodyH - 118f - 24f - 40f - Tw ;
for ( int i = 0 ; i < n ; i + + )
{
var ch = db . Chests . chests [ i ] ;
int k = i ;
bool sel = i = = chestIdx ;
var cell = UiKit . At ( p , "Chest_" + ch . id , tx0 + i * ( Tw + gap ) , ty , Tw , Tw ) ;
UiKit . ItemSlot ( cell , "chest_" + ch . id + "_" , sel ) ;
var hit = cell . gameObject . AddComponent < Image > ( ) ;
hit . color = Color . clear ;
cell . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ( ) = >
{
chestIdx = k ;
2026-11-09 09:23:18 +00:00
chestPicked = true ;
2026-11-09 09:12:53 +00:00
Refresh ( ) ;
} ) ;
var ic = UiKit . Icon ( cell , "chest_" + ch . id , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , 6f ) , 104f ) ;
if ( ic ! = null ) UiKit . DropShadow ( ic , 4f ) ;
if ( sel & & UISkin . Has ( "focus_line" ) )
{
2026-11-09 09:23:18 +00:00
var f = UiKit . Layer ( cell , "Focus" , "focus_line" , UiKit . Col ( "chest_select" , UiKit . White ) , - 8f , - 8f , - 8f , - 8f ) ;
2026-11-09 09:12:53 +00:00
f . gameObject . AddComponent < UiPulse > ( ) ;
2026-11-13 07:49:11 +00:00
// 1.08 배는 아래 가운데 기준(위·양옆으로만 커짐 = 아래끝·칩이 이름 쪽으로 내려오지 않음 · QA r1 B2 · r2 B3)
var pos = cell . anchoredPosition ;
cell . pivot = new Vector2 ( 0.5f , 0f ) ;
cell . anchoredPosition = pos + new Vector2 ( Tw * 0.5f , - Tw ) ;
2026-11-09 09:23:18 +00:00
cell . localScale = Vector3 . one * 1.08f ;
2026-11-09 09:12:53 +00:00
}
2026-11-09 09:23:18 +00:00
else UiKit . Layer ( cell , "Dim" , "item_bg" , UiKit . Col ( "chest_dim" , new Color ( 0f , 0f , 0f , 0.25f ) ) ) ;
2026-11-13 07:49:11 +00:00
// 수량 칩 = 선택 테두리 위(테두리가 칩을 덮지 않게 · QA r1 B2)
UiKit . QtyChip ( cell , "x" + TreasureRules . ChestCount ( d , ch . id ) , 28 ) ;
// 이름 = 모든 칸 같은 높이(선택 칸은 위로만 커짐 · 이름을 내리면 솟은 하단 「보물」 탭 뒤로 잘림 = QA r2 B3) · 금고 배경 = 외곽선(QA r1 M1)
float nameY = ty + Tw + 10f ;
var nl = UiKit . Label ( p , ch . nameKo , 26 , sel ? UiKit . White : soft , TextAnchor . UpperCenter , new Vector2 ( 0f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( tx0 + i * ( Tw + gap ) + Tw * 0.5f , - nameY ) , new Vector2 ( Tw + gap , 36f ) , sel ) ;
2026-11-09 09:12:53 +00:00
nl . horizontalOverflow = HorizontalWrapMode . Overflow ;
2026-11-13 07:49:11 +00:00
if ( ShownScene = = "chest" ) UiKit . Title ( nl , true ) ;
2026-11-09 09:12:53 +00:00
}
}
/// <summary>화살표 버튼(투명 판 + 팩 화살표 아이콘 · 없으면 글자).</summary>
static void Arrow ( Transform p , string name , string icon , float x , float y , System . Action onClick )
{
var rt = UiKit . At ( p , name , x , y , 100f , 140f ) ;
var img = rt . gameObject . AddComponent < Image > ( ) ;
img . color = Color . clear ;
rt . gameObject . AddComponent < Button > ( ) . onClick . AddListener ( ( ) = > onClick ( ) ) ;
var ic = UiKit . Icon ( rt , icon , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , 96f ) ;
if ( ic ! = null ) UiKit . DropShadow ( ic , 4f ) ;
else UiKit . Title ( UiKit . FillLabel ( rt , name = = "Prev" ? "◁" : "▷" , 70 , UiKit . White ) , true ) ;
}
2026-11-05 15:22:54 +00:00
void Step ( int dir )
{
int n = Mathf . Max ( 1 , S . Db . Chests . chests . Count ) ;
chestIdx = ( ( chestIdx + dir ) % n + n ) % n ;
2026-11-09 09:23:18 +00:00
chestPicked = true ;
2026-11-05 15:22:54 +00:00
Refresh ( ) ;
}
public void DoOpen ( string id )
{
var r = S . OpenChest ( id ) ;
LastOpen = r ;
if ( r = = null )
{
Root . Toast ( "상자 없음" ) ;
return ;
}
var it = r . treasureId ! = null ? S . Db . Treasures . Get ( r . treasureId ) : null ;
var sb = new System . Text . StringBuilder ( ) ;
if ( it ! = null )
sb . Append ( "<size=48><b>" ) . Append ( it . nameKo ) . Append ( "</b></size>\n" ) . Append ( TreasureRules . GradeKo ( it . grade ) ) . Append ( r . gradeUp ? " (상위 등급!)" : "" )
. Append ( r . isNew ? " <color=#b0301e>NEW</color>" : " 조각 +" + r . shards ) . Append ( "\n\n" ) ;
var parts = new List < string > ( ) ;
if ( r . gold > 0 ) parts . Add ( "골드 +" + r . gold . ToString ( "N0" ) ) ;
if ( r . gem > 0 ) parts . Add ( "보옥 +" + r . gem . ToString ( "N0" ) ) ;
if ( r . iron > 0 ) parts . Add ( "운철 +" + r . iron ) ;
parts . Add ( "상자 포인트 +" + r . points ) ;
sb . Append ( string . Join ( " · " , parts . ToArray ( ) ) ) ;
if ( r . bonusChests > 0 ) sb . Append ( "\n<color=#b0301e>포인트 달성 → " ) . Append ( S . Db . Chests . Get ( S . Db . Economy . ChestPointReward ) . nameKo ) . Append ( " +" ) . Append ( r . bonusChests ) . Append ( "</color>" ) ;
Root . Modal ( S . Db . Chests . Get ( id ) . nameKo + " 열기" , sb . ToString ( ) ,
new OutGameRoot . ModalButton ( "한 번 더" , UiKit . Red , ( ) = > DoOpen ( id ) ) ,
new OutGameRoot . ModalButton ( "확인" , UiKit . Dark , null ) ) ;
}
2026-11-09 09:12:53 +00:00
/// <summary>상자 대표 색 = 테마 chest_<id>(구 코드 상수 → ui_theme.csv · C45 · #1104).</summary>
2026-11-05 15:22:54 +00:00
public static Color ChestColor ( string id )
{
2026-11-09 09:12:53 +00:00
return UiKit . Col ( "chest_" + id , UiKit . Slot ) ;
2026-11-05 15:22:54 +00:00
}
}
}