명장 등용(뽑기 메뉴 개편) — 배너 5종(픽업 여포 + 국가별 상시 4) · 등용 버튼 · 상세 정보 팝업(PD 2026-11-13)

- 이름: 「영웅·유닛 뽑기」 → 「명장 등용」 · 버튼 「1회/10회 등용」 · 결과 제목 「등용 결과」
- 표: gacha_banners.csv 신설(배너 5 = type·faction·pool·soldiers·featured·pity_group·기간·art)
  gacha_pool.csv pickup 열 삭제(픽업 영웅 SOT = 배너 featured) · gacha.csv pickup_exclusive 추가
- 규칙: 배너별 풀(상시 = 그 진영 + 병사) · 픽업 = ★★★ 의 pickup_share · 천장 = pity_group 공유
  · 기간(start+days) 밖 등용 불가 · 세이브 gacha.pities(구 pity 는 상시 묶음으로 이관)
- 화면: 배너 2쪽 좌우 넘김 · 상시 = 지도 위 진영 깃발 4(↖위 ↙촉 ↗군 ↘오) + 주요 영웅 5 + 세로 이름표
  · 픽업 = 정보 판 + 여포 일러 + 이름 명패 + 「상세 정보」 · 자리 = Resources/UI/recruit_banner.csv
- 팝업 HeroInfoPopup: 풀 성장 능력치(최고 레벨·등급·★·장비) + 액티브/패시브 스킬
- 그림: UI/Samguk/recruit/(픽업 1 · 진영 배경 4 · 지도 1) · 아틀라스 UI_Recruit
- 테스트: RecruitTests 9/9 · OutGameTests·GachaCutsceneTests·UILayerTests 통과

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-13 22:34:16 +09:00
parent 0188f55202
commit a6bd9c0f66
43 changed files with 2744 additions and 319 deletions

View File

@ -6,6 +6,7 @@ p_r2,0.15
p_r1,0.82
pity,80
pickup_share,0.5
pickup_exclusive,1
free_first_ten,1
daily_free,1
ten_count,10

1 key value
6 p_r1 0.82
7 pity 80
8 pickup_share 0.5
9 pickup_exclusive 1
10 free_first_ten 1
11 daily_free 1
12 ten_count 10

View File

@ -0,0 +1,6 @@
banner_id,type,faction,name_ko,desc_ko,pool,soldiers,featured,pity_group,start,days,art
pickup_lvbu,pickup,other,여포 등용,픽업 영웅의 등장 확률 상승,all,1,lvbu,pickup,2026-11-13,14,recruit_pickup_lvbu
wei,standing,wei,위 명장 등용,위나라의 명장을 등용합니다,wei,1,caocao;xiahoudun;zhangliao;simayi;xuchu,standing,,0,recruit_bg_wei
shu,standing,shu,촉 명장 등용,촉나라의 명장을 등용합니다,shu,1,liubei;guanyu;zhangfei;zhaoyun;zhugeliang,standing,,0,recruit_bg_shu
wu,standing,wu,오 명장 등용,오나라의 명장을 등용합니다,wu,1,sunquan;zhouyu;sunce;luxun;sunshangxiang,standing,,0,recruit_bg_wu
other,standing,other,군웅 명장 등용,천하를 다툰 군웅을 등용합니다,other,1,dongzhuo;yuanshao;diaochan;menghuo;zhurong,standing,,0,recruit_bg_other
1 banner_id type faction name_ko desc_ko pool soldiers featured pity_group start days art
2 pickup_lvbu pickup other 여포 등용 픽업 영웅의 등장 확률 상승 all 1 lvbu pickup 2026-11-13 14 recruit_pickup_lvbu
3 wei standing wei 위 명장 등용 위나라의 명장을 등용합니다 wei 1 caocao;xiahoudun;zhangliao;simayi;xuchu standing 0 recruit_bg_wei
4 shu standing shu 촉 명장 등용 촉나라의 명장을 등용합니다 shu 1 liubei;guanyu;zhangfei;zhaoyun;zhugeliang standing 0 recruit_bg_shu
5 wu standing wu 오 명장 등용 오나라의 명장을 등용합니다 wu 1 sunquan;zhouyu;sunce;luxun;sunshangxiang standing 0 recruit_bg_wu
6 other standing other 군웅 명장 등용 천하를 다툰 군웅을 등용합니다 other 1 dongzhuo;yuanshao;diaochan;menghuo;zhurong standing 0 recruit_bg_other

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3449dfa5d4016d04aa104617f4119126
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,88 +1,88 @@
hero_id,in_pool,pickup
liubei,1,0
guanyu,1,0
zhangfei,1,0
zhaoyun,1,1
huangzhong,1,0
zhugeliang,1,0
zhoucang,1,0
guanping,1,0
xushu,1,0
taishici,1,0
machao,1,0
weiyan,1,0
pangtong,1,0
jiangwei,1,0
liufeng,1,0
jianyong,1,0
sunqian,1,0
mizhu,1,0
caocao,1,0
xiahoudun,1,0
xuchu,1,0
zhangliao,1,1
xuhuang,1,0
zhanghe,1,0
caoren,1,0
yujin,1,0
simayi,1,0
xiahouyuan,1,0
guojia,1,0
jiaxu,1,0
dianwei,1,0
xunyu,1,0
xiahoumao,1,0
wanglang,1,0
xiahouen,1,0
sunjian,1,0
zhouyu,1,0
huanggai,1,0
ganning,1,0
chengpu,1,0
sunce,1,0
sunquan,1,0
lusu,1,0
lvmeng,1,0
luxun,1,0
sunshangxiang,1,0
xiaoqiao,1,0
lingtong,1,0
zhoutai,1,0
zhugejin,1,0
dongxi,1,0
lvbu,1,0
zhangjiao,1,0
huaxiong,1,0
yanliang,1,0
wenchou,1,0
gongsunzan,1,0
dongzhuo,1,0
lijue,1,0
yuanshao,1,0
yuanshu,1,0
diaochan,1,0
zhurong,1,0
menghuo,1,0
gaoshun,1,0
zhanglu,1,0
yanbaihu,1,0
hanxuan,1,0
jinxuan,1,0
zhaofan,1,0
zhangren,1,0
chunyuqiong,1,0
soldier_infantry_1,1,0
soldier_infantry_2,1,0
soldier_infantry_3,1,0
soldier_spear_1,1,0
soldier_spear_2,1,0
soldier_spear_3,1,0
soldier_cavalry_1,1,0
soldier_cavalry_2,1,0
soldier_cavalry_3,1,0
soldier_archer_1,1,0
soldier_archer_2,1,0
soldier_archer_3,1,0
soldier_strategist_1,1,0
soldier_strategist_2,1,0
soldier_strategist_3,1,0
hero_id,in_pool
liubei,1
guanyu,1
zhangfei,1
zhaoyun,1
huangzhong,1
zhugeliang,1
zhoucang,1
guanping,1
xushu,1
taishici,1
machao,1
weiyan,1
pangtong,1
jiangwei,1
liufeng,1
jianyong,1
sunqian,1
mizhu,1
caocao,1
xiahoudun,1
xuchu,1
zhangliao,1
xuhuang,1
zhanghe,1
caoren,1
yujin,1
simayi,1
xiahouyuan,1
guojia,1
jiaxu,1
dianwei,1
xunyu,1
xiahoumao,1
wanglang,1
xiahouen,1
sunjian,1
zhouyu,1
huanggai,1
ganning,1
chengpu,1
sunce,1
sunquan,1
lusu,1
lvmeng,1
luxun,1
sunshangxiang,1
xiaoqiao,1
lingtong,1
zhoutai,1
zhugejin,1
dongxi,1
lvbu,1
zhangjiao,1
huaxiong,1
yanliang,1
wenchou,1
gongsunzan,1
dongzhuo,1
lijue,1
yuanshao,1
yuanshu,1
diaochan,1
zhurong,1
menghuo,1
gaoshun,1
zhanglu,1
yanbaihu,1
hanxuan,1
jinxuan,1
zhaofan,1
zhangren,1
chunyuqiong,1
soldier_infantry_1,1
soldier_infantry_2,1
soldier_infantry_3,1
soldier_spear_1,1
soldier_spear_2,1
soldier_spear_3,1
soldier_cavalry_1,1
soldier_cavalry_2,1
soldier_cavalry_3,1
soldier_archer_1,1
soldier_archer_2,1
soldier_archer_3,1
soldier_strategist_1,1
soldier_strategist_2,1
soldier_strategist_3,1

1 hero_id in_pool pickup
2 liubei 1 0
3 guanyu 1 0
4 zhangfei 1 0
5 zhaoyun 1 1
6 huangzhong 1 0
7 zhugeliang 1 0
8 zhoucang 1 0
9 guanping 1 0
10 xushu 1 0
11 taishici 1 0
12 machao 1 0
13 weiyan 1 0
14 pangtong 1 0
15 jiangwei 1 0
16 liufeng 1 0
17 jianyong 1 0
18 sunqian 1 0
19 mizhu 1 0
20 caocao 1 0
21 xiahoudun 1 0
22 xuchu 1 0
23 zhangliao 1 1
24 xuhuang 1 0
25 zhanghe 1 0
26 caoren 1 0
27 yujin 1 0
28 simayi 1 0
29 xiahouyuan 1 0
30 guojia 1 0
31 jiaxu 1 0
32 dianwei 1 0
33 xunyu 1 0
34 xiahoumao 1 0
35 wanglang 1 0
36 xiahouen 1 0
37 sunjian 1 0
38 zhouyu 1 0
39 huanggai 1 0
40 ganning 1 0
41 chengpu 1 0
42 sunce 1 0
43 sunquan 1 0
44 lusu 1 0
45 lvmeng 1 0
46 luxun 1 0
47 sunshangxiang 1 0
48 xiaoqiao 1 0
49 lingtong 1 0
50 zhoutai 1 0
51 zhugejin 1 0
52 dongxi 1 0
53 lvbu 1 0
54 zhangjiao 1 0
55 huaxiong 1 0
56 yanliang 1 0
57 wenchou 1 0
58 gongsunzan 1 0
59 dongzhuo 1 0
60 lijue 1 0
61 yuanshao 1 0
62 yuanshu 1 0
63 diaochan 1 0
64 zhurong 1 0
65 menghuo 1 0
66 gaoshun 1 0
67 zhanglu 1 0
68 yanbaihu 1 0
69 hanxuan 1 0
70 jinxuan 1 0
71 zhaofan 1 0
72 zhangren 1 0
73 chunyuqiong 1 0
74 soldier_infantry_1 1 0
75 soldier_infantry_2 1 0
76 soldier_infantry_3 1 0
77 soldier_spear_1 1 0
78 soldier_spear_2 1 0
79 soldier_spear_3 1 0
80 soldier_cavalry_1 1 0
81 soldier_cavalry_2 1 0
82 soldier_cavalry_3 1 0
83 soldier_archer_1 1 0
84 soldier_archer_2 1 0
85 soldier_archer_3 1 0
86 soldier_strategist_1 1 0
87 soldier_strategist_2 1 0
88 soldier_strategist_3 1 0

View File

@ -4,6 +4,7 @@ theme,UI_SkillIcon,/skill_icon/,2048,장수 스킬 아이콘 144 + 사각 틀
theme,UI_Lobby,/Lobby/anim/,1024,로비 배경 연출(깃발·불빛·새·꽃잎 9장 · PD 2026-11-11) — Frame 층 한 장 = 드로우콜 1
theme,UI_Domestic,/Domestic/anim/|/Domestic/icon/|/Domestic/dev/,1024,내정 연병장 풀·꽃 덤불 4 · 훈련 카드 아이콘 3 · 개발 노드 아이콘 27(PD 2026-11-12) — 128² 이하 · 내정 화면 한 장
theme,UI_Gacha,/Samguk/gacha/,2048,뽑기 연출(삼고초려 · PD 2026-11-13) 문짝 2 · 햇살 2 · 빛줄기 2 · 반짝이 · 먼지 구름 · 고리 · 느낌표 — 장면 Frame 층 한 장 = 드로우콜 1
theme,UI_Recruit,/Samguk/recruit/|/Samguk/flag_rank/,1024,명장 등용 배너(PD 2026-11-13) 지도 두루마리 1 + 진영 깃발 4 — 배너 층 한 장 · 배너 그림 5(1016×624)는 single_area 초과라 단독
theme,UI_Common,*,4096,모든 화면 공통 크롬(틀·버튼·바·탭·재화·병과·작은 아이콘) — 위 줄에 안 걸린 테마 스프라이트 전부
folder,WorldMap_UI,Assets/Resources/ChapterMap/UI,2048,월드맵 부품(깃발·띠·판·별·커서) — 폴더 전체
option,single_area,,262144,이 넓이(px) 넘는 큰 그림(로비 바탕·키아트·로고·패턴)은 아틀라스에 넣지 않음 — 한 장 = 드로우콜 1 · 아틀라스 공간 낭비 방지

1 kind name match value note
4 theme UI_Lobby /Lobby/anim/ 1024 로비 배경 연출(깃발·불빛·새·꽃잎 9장 · PD 2026-11-11) — Frame 층 한 장 = 드로우콜 1
5 theme UI_Domestic /Domestic/anim/|/Domestic/icon/|/Domestic/dev/ 1024 내정 연병장 풀·꽃 덤불 4 · 훈련 카드 아이콘 3 · 개발 노드 아이콘 27(PD 2026-11-12) — 128² 이하 · 내정 화면 한 장
6 theme UI_Gacha /Samguk/gacha/ 2048 뽑기 연출(삼고초려 · PD 2026-11-13) 문짝 2 · 햇살 2 · 빛줄기 2 · 반짝이 · 먼지 구름 · 고리 · 느낌표 — 장면 Frame 층 한 장 = 드로우콜 1
7 theme UI_Recruit /Samguk/recruit/|/Samguk/flag_rank/ 1024 명장 등용 배너(PD 2026-11-13) 지도 두루마리 1 + 진영 깃발 4 — 배너 층 한 장 · 배너 그림 5(1016×624)는 single_area 초과라 단독
8 theme UI_Common * 4096 모든 화면 공통 크롬(틀·버튼·바·탭·재화·병과·작은 아이콘) — 위 줄에 안 걸린 테마 스프라이트 전부
9 folder WorldMap_UI Assets/Resources/ChapterMap/UI 2048 월드맵 부품(깃발·띠·판·별·커서) — 폴더 전체
10 option single_area 262144 이 넓이(px) 넘는 큰 그림(로비 바탕·키아트·로고·패턴)은 아틀라스에 넣지 않음 — 한 장 = 드로우콜 1 · 아틀라스 공간 낭비 방지

View File

@ -28,6 +28,8 @@ MonoBehaviour:
value: 80
- key: pickup_share
value: 0.5
- key: pickup_exclusive
value: 1
- key: free_first_ten
value: 1
- key: daily_free
@ -39,262 +41,262 @@ MonoBehaviour:
pool:
- heroId: liubei
inPool: 1
pickup: 0
- heroId: guanyu
inPool: 1
pickup: 0
- heroId: zhangfei
inPool: 1
pickup: 0
- heroId: zhaoyun
inPool: 1
pickup: 1
- heroId: huangzhong
inPool: 1
pickup: 0
- heroId: zhugeliang
inPool: 1
pickup: 0
- heroId: zhoucang
inPool: 1
pickup: 0
- heroId: guanping
inPool: 1
pickup: 0
- heroId: xushu
inPool: 1
pickup: 0
- heroId: taishici
inPool: 1
pickup: 0
- heroId: machao
inPool: 1
pickup: 0
- heroId: weiyan
inPool: 1
pickup: 0
- heroId: pangtong
inPool: 1
pickup: 0
- heroId: jiangwei
inPool: 1
pickup: 0
- heroId: liufeng
inPool: 1
pickup: 0
- heroId: jianyong
inPool: 1
pickup: 0
- heroId: sunqian
inPool: 1
pickup: 0
- heroId: mizhu
inPool: 1
pickup: 0
- heroId: caocao
inPool: 1
pickup: 0
- heroId: xiahoudun
inPool: 1
pickup: 0
- heroId: xuchu
inPool: 1
pickup: 0
- heroId: zhangliao
inPool: 1
pickup: 1
- heroId: xuhuang
inPool: 1
pickup: 0
- heroId: zhanghe
inPool: 1
pickup: 0
- heroId: caoren
inPool: 1
pickup: 0
- heroId: yujin
inPool: 1
pickup: 0
- heroId: simayi
inPool: 1
pickup: 0
- heroId: xiahouyuan
inPool: 1
pickup: 0
- heroId: guojia
inPool: 1
pickup: 0
- heroId: jiaxu
inPool: 1
pickup: 0
- heroId: dianwei
inPool: 1
pickup: 0
- heroId: xunyu
inPool: 1
pickup: 0
- heroId: xiahoumao
inPool: 1
pickup: 0
- heroId: wanglang
inPool: 1
pickup: 0
- heroId: xiahouen
inPool: 1
pickup: 0
- heroId: sunjian
inPool: 1
pickup: 0
- heroId: zhouyu
inPool: 1
pickup: 0
- heroId: huanggai
inPool: 1
pickup: 0
- heroId: ganning
inPool: 1
pickup: 0
- heroId: chengpu
inPool: 1
pickup: 0
- heroId: sunce
inPool: 1
pickup: 0
- heroId: sunquan
inPool: 1
pickup: 0
- heroId: lusu
inPool: 1
pickup: 0
- heroId: lvmeng
inPool: 1
pickup: 0
- heroId: luxun
inPool: 1
pickup: 0
- heroId: sunshangxiang
inPool: 1
pickup: 0
- heroId: xiaoqiao
inPool: 1
pickup: 0
- heroId: lingtong
inPool: 1
pickup: 0
- heroId: zhoutai
inPool: 1
pickup: 0
- heroId: zhugejin
inPool: 1
pickup: 0
- heroId: dongxi
inPool: 1
pickup: 0
- heroId: lvbu
inPool: 1
pickup: 0
- heroId: zhangjiao
inPool: 1
pickup: 0
- heroId: huaxiong
inPool: 1
pickup: 0
- heroId: yanliang
inPool: 1
pickup: 0
- heroId: wenchou
inPool: 1
pickup: 0
- heroId: gongsunzan
inPool: 1
pickup: 0
- heroId: dongzhuo
inPool: 1
pickup: 0
- heroId: lijue
inPool: 1
pickup: 0
- heroId: yuanshao
inPool: 1
pickup: 0
- heroId: yuanshu
inPool: 1
pickup: 0
- heroId: diaochan
inPool: 1
pickup: 0
- heroId: zhurong
inPool: 1
pickup: 0
- heroId: menghuo
inPool: 1
pickup: 0
- heroId: gaoshun
inPool: 1
pickup: 0
- heroId: zhanglu
inPool: 1
pickup: 0
- heroId: yanbaihu
inPool: 1
pickup: 0
- heroId: hanxuan
inPool: 1
pickup: 0
- heroId: jinxuan
inPool: 1
pickup: 0
- heroId: zhaofan
inPool: 1
pickup: 0
- heroId: zhangren
inPool: 1
pickup: 0
- heroId: chunyuqiong
inPool: 1
pickup: 0
- heroId: soldier_infantry_1
inPool: 1
pickup: 0
- heroId: soldier_infantry_2
inPool: 1
pickup: 0
- heroId: soldier_infantry_3
inPool: 1
pickup: 0
- heroId: soldier_spear_1
inPool: 1
pickup: 0
- heroId: soldier_spear_2
inPool: 1
pickup: 0
- heroId: soldier_spear_3
inPool: 1
pickup: 0
- heroId: soldier_cavalry_1
inPool: 1
pickup: 0
- heroId: soldier_cavalry_2
inPool: 1
pickup: 0
- heroId: soldier_cavalry_3
inPool: 1
pickup: 0
- heroId: soldier_archer_1
inPool: 1
pickup: 0
- heroId: soldier_archer_2
inPool: 1
pickup: 0
- heroId: soldier_archer_3
inPool: 1
pickup: 0
- heroId: soldier_strategist_1
inPool: 1
pickup: 0
- heroId: soldier_strategist_2
inPool: 1
pickup: 0
- heroId: soldier_strategist_3
inPool: 1
pickup: 0
banners:
- id: pickup_lvbu
type: pickup
faction: other
nameKo: "\uC5EC\uD3EC \uB4F1\uC6A9"
descKo: "\uD53D\uC5C5 \uC601\uC6C5\uC758 \uB4F1\uC7A5 \uD655\uB960 \uC0C1\uC2B9"
pool:
- all
soldiers: 1
featured:
- lvbu
pityGroup: pickup
start: 2026-11-13
days: 14
art: recruit_pickup_lvbu
- id: wei
type: standing
faction: wei
nameKo: "\uC704 \uBA85\uC7A5 \uB4F1\uC6A9"
descKo: "\uC704\uB098\uB77C\uC758 \uBA85\uC7A5\uC744 \uB4F1\uC6A9\uD569\uB2C8\uB2E4"
pool:
- wei
soldiers: 1
featured:
- caocao
- xiahoudun
- zhangliao
- simayi
- xuchu
pityGroup: standing
start:
days: 0
art: recruit_bg_wei
- id: shu
type: standing
faction: shu
nameKo: "\uCD09 \uBA85\uC7A5 \uB4F1\uC6A9"
descKo: "\uCD09\uB098\uB77C\uC758 \uBA85\uC7A5\uC744 \uB4F1\uC6A9\uD569\uB2C8\uB2E4"
pool:
- shu
soldiers: 1
featured:
- liubei
- guanyu
- zhangfei
- zhaoyun
- zhugeliang
pityGroup: standing
start:
days: 0
art: recruit_bg_shu
- id: wu
type: standing
faction: wu
nameKo: "\uC624 \uBA85\uC7A5 \uB4F1\uC6A9"
descKo: "\uC624\uB098\uB77C\uC758 \uBA85\uC7A5\uC744 \uB4F1\uC6A9\uD569\uB2C8\uB2E4"
pool:
- wu
soldiers: 1
featured:
- sunquan
- zhouyu
- sunce
- luxun
- sunshangxiang
pityGroup: standing
start:
days: 0
art: recruit_bg_wu
- id: other
type: standing
faction: other
nameKo: "\uAD70\uC6C5 \uBA85\uC7A5 \uB4F1\uC6A9"
descKo: "\uCC9C\uD558\uB97C \uB2E4\uD230 \uAD70\uC6C5\uC744 \uB4F1\uC6A9\uD569\uB2C8\uB2E4"
pool:
- other
soldiers: 1
featured:
- dongzhuo
- yuanshao
- diaochan
- menghuo
- zhurong
pityGroup: standing
start:
days: 0
art: recruit_bg_other

View File

@ -2232,6 +2232,30 @@ MonoBehaviour:
pack: {fileID: 0}
samguk: {fileID: 21300000, guid: c7d5c5fdb0f26c34db40248bc372ddfb, type: 3}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: recruit_bg_other
pack: {fileID: 0}
samguk: {fileID: 21300000, guid: b2b554a86e7fbd44092b455ec9603057, type: 3}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: recruit_bg_shu
pack: {fileID: 0}
samguk: {fileID: 21300000, guid: 7be45317ea022cb4eb13eb935a6f4975, type: 3}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: recruit_bg_wei
pack: {fileID: 0}
samguk: {fileID: 21300000, guid: f0f9add628d14e74eb70edfdb0089368, type: 3}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: recruit_bg_wu
pack: {fileID: 0}
samguk: {fileID: 21300000, guid: 0bfef021aaab1244d87e8b5666d3d9ae, type: 3}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: recruit_map
pack: {fileID: 0}
samguk: {fileID: 21300000, guid: 277f35bc5cb3e6d48b867fc870307944, type: 3}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: recruit_pickup_lvbu
pack: {fileID: 0}
samguk: {fileID: 21300000, guid: 5279d758233ccdd488a3e2df62689fc9, type: 3}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: skill_caocao_a
pack: {fileID: 0}
samguk: {fileID: 21300000, guid: 6cdc5ccaa86cab244912eeeb49450c82, type: 3}

View File

@ -0,0 +1,30 @@
# 명장 등용 배너 배치 표(PD 2026-11-13 「뽑기 배너 영역은 예시와 같이 · 위/촉/오/군 국가별 상시 배너에 주요 영웅 5명 · 상시 배너 왼쪽 = 중국 대륙 형상 위에 진영 깃발을 마름모꼴로 = 버튼 · ↖위 ↙촉 ↗군 ↘오」)
# 형식 = 로비 장면 표(LobbyScene.Load) · 좌표 = 배너 그림 px(왼쪽 위 0,0 · 아래로 +) · 배너 그림을 바꾸면 이 표만 고친다(C45) · 누가 나오는지·이름·기간 = Assets/Data/OutGame/gacha_banners.csv
# view f_w,f_h = 좌표 기준 크기(배너 그림 크기)
# map 지도 두루마리: s_sprite 그림 · f_x,f_y 왼쪽 위 · f_w,f_h 크기
# flag_<진영> 진영 깃발 버튼(진영 = gacha_banners.csv faction): s_sprite 깃발 그림 · f_x,f_y = 지도 두루마리 왼쪽 위에서 깃발 가운데까지 · f_w 크기 · f_min 고르지 않은 깃발 배율 · f_max 고른 깃발 배율 · f_shade 고르지 않은 깃발 밝기
# flag_glow 고른 깃발 밑 빛: s_sprite · f_w 지름 · s_color 색 · f_min~f_max 알파 맥동 · f_period 주기(초)
# hero 주요 영웅 자리(i_n = 인원 · i_slot = featured 순서 0 가운데 앞 · 1 왼쪽 앞 · 2 오른쪽 앞 · 3 왼쪽 뒤 · 4 오른쪽 뒤): f_x,f_y 발 · f_h 표준 키(UnitStand · 로비 영웅과 같은 기준) · f_max 말 탄 영웅 머리 끝 상한(키의 배수)
# tag 세로 이름표(i_slot = 영웅 자리): s_sprite 목패(등급 색 목패 재사용 · 세워서 씀) · f_x = 가운데 · f_y = 윗변(i_count 1 이면 아랫변 = 이름 길이와 상관없이 머리 바로 위에 붙임) · f_w 폭 · f_h 글자 1자 높이 · f_scale 글자 크기 · f_min 위아래 여백 · s_color 판 색
# hit 영웅 누름 칸(상세 정보 팝업): f_w,f_h = 발 위로 잡는 칸 크기
# shadow 영웅 발밑 그림자: s_sprite · f_w,f_h = 표준 키의 배수(폭 · 높이) · s_color
s_kind,i_n,i_slot,i_count,s_sprite,f_x,f_y,f_w,f_h,f_scale,f_min,f_max,f_shade,f_period,s_color,s_note
view,,,,,,,1016,624,,,,,,,배너 그림 1016×624(배너 틀 1032×640 안쪽 8)
map,,,,recruit_map,58,106,380,384,,,,,,,중국 대륙 지도 두루마리(Krea 2 seed 374 · 흰 바탕 → 투명)
flag_wei,,,,flag_wei,150,150,92,,,0.86,1.14,0.78,,,↖ 위(북쪽 가운데)
flag_other,,,,flag_other,262,124,92,,,0.86,1.14,0.78,,,↗ 군(북동쪽)
flag_shu,,,,flag_shu,126,252,92,,,0.86,1.14,0.78,,,↙ 촉(남서쪽)
flag_wu,,,,flag_wu,246,264,92,,,0.86,1.14,0.78,,,↘ 오(남동쪽)
flag_glow,,,,glow_circle,,,178,,,0.6,1.0,,0.9,#FFE27A,고른 깃발 밑 금빛(맥동)
hero,5,3,,,622,414,,186,,,1.25,,,,왼쪽 뒤
hero,5,4,,,846,414,,186,,,1.25,,,,오른쪽 뒤
hero,5,1,,,566,602,,186,,,1.25,,,,왼쪽 앞
hero,5,2,,,896,602,,186,,,1.25,,,,오른쪽 앞
hero,5,0,,,731,606,,186,,,1.25,,,,가운데 앞(주인공)
tag,5,3,,tre_tag,528,196,46,30,26,14,,,,#C7372A,왼쪽 뒤 영웅의 왼쪽 위
tag,5,4,,tre_tag,938,150,46,30,26,14,,,,#C7372A,오른쪽 뒤 영웅의 오른쪽 위(화살표 위쪽)
tag,5,0,1,tre_tag,733,418,46,30,26,14,,,,#D99A1E,가운데 영웅 머리 바로 위(아랫변 기준 · 뒤 두 사람 사이 빈틈 · 주인공 = 금빛 판으로 구분)
tag,5,1,,tre_tag,468,398,46,30,26,14,,,,#C7372A,왼쪽 앞 영웅의 왼쪽
tag,5,2,,tre_tag,986,398,46,30,26,14,,,,#C7372A,오른쪽 앞 영웅의 오른쪽
hit,,,,,,,150,190,,,,,,,영웅 누름 칸
shadow,,,,oval_shadow,,,0.66,0.15,,,,,,#00000070,발밑 그림자(f_w,f_h = 표준 키의 배수)
1 # 명장 등용 배너 배치 표(PD 2026-11-13 「뽑기 배너 영역은 예시와 같이 · 위/촉/오/군 국가별 상시 배너에 주요 영웅 5명 · 상시 배너 왼쪽 = 중국 대륙 형상 위에 진영 깃발을 마름모꼴로 = 버튼 · ↖위 ↙촉 ↗군 ↘오」)
2 # 형식 = 로비 장면 표(LobbyScene.Load) · 좌표 = 배너 그림 px(왼쪽 위 0,0 · 아래로 +) · 배너 그림을 바꾸면 이 표만 고친다(C45) · 누가 나오는지·이름·기간 = Assets/Data/OutGame/gacha_banners.csv
3 # view f_w,f_h = 좌표 기준 크기(배너 그림 크기)
4 # map 지도 두루마리: s_sprite 그림 · f_x,f_y 왼쪽 위 · f_w,f_h 크기
5 # flag_<진영> 진영 깃발 버튼(진영 = gacha_banners.csv faction): s_sprite 깃발 그림 · f_x,f_y = 지도 두루마리 왼쪽 위에서 깃발 가운데까지 · f_w 크기 · f_min 고르지 않은 깃발 배율 · f_max 고른 깃발 배율 · f_shade 고르지 않은 깃발 밝기
6 # flag_glow 고른 깃발 밑 빛: s_sprite · f_w 지름 · s_color 색 · f_min~f_max 알파 맥동 · f_period 주기(초)
7 # hero 주요 영웅 자리(i_n = 인원 · i_slot = featured 순서 0 가운데 앞 · 1 왼쪽 앞 · 2 오른쪽 앞 · 3 왼쪽 뒤 · 4 오른쪽 뒤): f_x,f_y 발 · f_h 표준 키(UnitStand · 로비 영웅과 같은 기준) · f_max 말 탄 영웅 머리 끝 상한(키의 배수)
8 # tag 세로 이름표(i_slot = 영웅 자리): s_sprite 목패(등급 색 목패 재사용 · 세워서 씀) · f_x = 가운데 · f_y = 윗변(i_count 1 이면 아랫변 = 이름 길이와 상관없이 머리 바로 위에 붙임) · f_w 폭 · f_h 글자 1자 높이 · f_scale 글자 크기 · f_min 위아래 여백 · s_color 판 색
9 # hit 영웅 누름 칸(상세 정보 팝업): f_w,f_h = 발 위로 잡는 칸 크기
10 # shadow 영웅 발밑 그림자: s_sprite · f_w,f_h = 표준 키의 배수(폭 · 높이) · s_color
11 s_kind,i_n,i_slot,i_count,s_sprite,f_x,f_y,f_w,f_h,f_scale,f_min,f_max,f_shade,f_period,s_color,s_note
12 view,,,,,,,1016,624,,,,,,,배너 그림 1016×624(배너 틀 1032×640 안쪽 8)
13 map,,,,recruit_map,58,106,380,384,,,,,,,중국 대륙 지도 두루마리(Krea 2 seed 374 · 흰 바탕 → 투명)
14 flag_wei,,,,flag_wei,150,150,92,,,0.86,1.14,0.78,,,↖ 위(북쪽 가운데)
15 flag_other,,,,flag_other,262,124,92,,,0.86,1.14,0.78,,,↗ 군(북동쪽)
16 flag_shu,,,,flag_shu,126,252,92,,,0.86,1.14,0.78,,,↙ 촉(남서쪽)
17 flag_wu,,,,flag_wu,246,264,92,,,0.86,1.14,0.78,,,↘ 오(남동쪽)
18 flag_glow,,,,glow_circle,,,178,,,0.6,1.0,,0.9,#FFE27A,고른 깃발 밑 금빛(맥동)
19 hero,5,3,,,622,414,,186,,,1.25,,,,왼쪽 뒤
20 hero,5,4,,,846,414,,186,,,1.25,,,,오른쪽 뒤
21 hero,5,1,,,566,602,,186,,,1.25,,,,왼쪽 앞
22 hero,5,2,,,896,602,,186,,,1.25,,,,오른쪽 앞
23 hero,5,0,,,731,606,,186,,,1.25,,,,가운데 앞(주인공)
24 tag,5,3,,tre_tag,528,196,46,30,26,14,,,,#C7372A,왼쪽 뒤 영웅의 왼쪽 위
25 tag,5,4,,tre_tag,938,150,46,30,26,14,,,,#C7372A,오른쪽 뒤 영웅의 오른쪽 위(화살표 위쪽)
26 tag,5,0,1,tre_tag,733,418,46,30,26,14,,,,#D99A1E,가운데 영웅 머리 바로 위(아랫변 기준 · 뒤 두 사람 사이 빈틈 · 주인공 = 금빛 판으로 구분)
27 tag,5,1,,tre_tag,468,398,46,30,26,14,,,,#C7372A,왼쪽 앞 영웅의 왼쪽
28 tag,5,2,,tre_tag,986,398,46,30,26,14,,,,#C7372A,오른쪽 앞 영웅의 오른쪽
29 hit,,,,,,,150,190,,,,,,,영웅 누름 칸
30 shadow,,,,oval_shadow,,,0.66,0.15,,,,,,#00000070,발밑 그림자(f_w,f_h = 표준 키의 배수)

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4ad4f3108beb9724cbf2125fb958d455
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -4,24 +4,65 @@ using UnityEngine;
namespace TKS.OutGame
{
/// <summary>영웅·유닛 뽑기(gacha.csv key,value + gacha_pool.csv · 기획서 §4). 병사 유닛 = ★ 풀(§4).</summary>
/// <summary>
/// 명장 등용(뽑기 · gacha.csv key,value + gacha_pool.csv + gacha_banners.csv · 기획서 §4 · PD 2026-11-13 배너 5 = 픽업 1 + 국가별 상시 4).
/// 병사 유닛 = ★ 풀(§4) · 픽업 영웅 = 배너 표 featured(픽업 배너에서 ★★★ 의 pickup_share).
/// </summary>
public class GachaConfig : ScriptableObject
{
public const string TypePickup = "pickup";
public const string TypeStanding = "standing";
public const string PoolAll = "all";
[Serializable]
public class PoolEntry
{
public string heroId;
public bool inPool = true;
public bool pickup;
}
/// <summary>등용 배너 1줄(gacha_banners.csv · 줄 순서 = 화면 순서).</summary>
[Serializable]
public class Banner
{
public string id;
[Tooltip("pickup = 기간 한정 · 주요 영웅 확률 상승 / standing = 상시(국가별)")]
public string type;
[Tooltip("상시 배너의 진영(wei · shu · wu · other · 깃발 버튼)")]
public string faction;
public string nameKo;
public string descKo;
[Tooltip("나오는 진영(heroes.csv faction · all = 전 진영)")]
public List<string> pool = new List<string>();
[Tooltip("병사 유닛(★ 풀)이 나오는가")]
public bool soldiers;
[Tooltip("배너에 세우는 주요 영웅(픽업 = 확률 상승 대상)")]
public List<string> featured = new List<string>();
[Tooltip("천장을 같이 쓰는 묶음 이름")]
public string pityGroup;
[Tooltip("시작일 yyyy-MM-dd(UTC) · 빈 칸 = 기간 없음")]
public string start;
[Tooltip("기간(일) · 0 = 기간 없음")]
public int days;
[Tooltip("배너 그림(테마 아이콘 id)")]
public string art;
public bool IsPickup
{
get { return type == TypePickup; }
}
}
public KeyValueList values = new KeyValueList();
public List<PoolEntry> pool = new List<PoolEntry>();
public List<Banner> banners = new List<Banner>();
public int CostSingle { get { return values.I("cost_single"); } }
public int CostTen { get { return values.I("cost_ten"); } }
public int Pity { get { return values.I("pity"); } }
public float PickupShare { get { return values.F("pickup_share"); } }
/// <summary>픽업 영웅은 픽업 배너에서만 나온다(상시 배너 풀에서 뺌).</summary>
public bool PickupExclusive { get { return values.I("pickup_exclusive") > 0; } }
public bool FreeFirstTen { get { return values.I("free_first_ten") > 0; } }
public int TenCount { get { return values.I("ten_count"); } }
public int TenGuaranteeRarity { get { return values.I("ten_guarantee_rarity"); } }
@ -32,9 +73,16 @@ namespace TKS.OutGame
return values.F("p_r" + rarity);
}
public Banner Get(string id)
{
foreach (var b in banners)
if (b.id == id) return b;
return null;
}
public static readonly string[] RequiredKeys =
{
"cost_single", "cost_ten", "p_r3", "p_r2", "p_r1", "pity", "pickup_share", "free_first_ten", "ten_count", "ten_guarantee_rarity",
"cost_single", "cost_ten", "p_r3", "p_r2", "p_r1", "pity", "pickup_share", "pickup_exclusive", "free_first_ten", "ten_count", "ten_guarantee_rarity",
};
}
}

View File

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
namespace TKS.OutGame
@ -17,8 +19,10 @@ namespace TKS.OutGame
}
/// <summary>
/// 영웅·유닛 뽑기(기획서 §4 · gacha.csv · gacha_pool.csv): 등급 확률 p_r3/p_r2/p_r1 · 천장 = pity 번째 ★★★ 확정(획득 시 0) ·
/// ★★★ 중 pickup_share 는 픽업 영웅 · 10연 = ten_guarantee_rarity 이상 1 보장 · 병사 유닛 = ★ 풀 · 중복 = 조각(shard_dup_r*) / 병사 = 골드(soldier_dup_gold) · 첫 10연 무료.
/// 명장 등용(뽑기 · 기획서 §4 · gacha.csv · gacha_pool.csv · gacha_banners.csv): 등급 확률 p_r3/p_r2/p_r1 · 천장 = pity 번째 ★★★ 확정(획득 시 0 · 배너의 pity_group 끼리 같이 씀) ·
/// 10연 = ten_guarantee_rarity 이상 1 보장 · 병사 유닛 = ★ 풀 · 중복 = 조각(shard_dup_r*) / 병사 = 골드(soldier_dup_gold) · 첫 10연 무료(배너 공통 1번).
/// 배너(PD 2026-11-13): 상시(standing) = 그 배너 pool 진영의 영웅만(+ soldiers) · 픽업(pickup) = ★★★ 중 pickup_share 는 featured 영웅 ·
/// pickup_exclusive = 픽업 영웅은 상시 배너에서 안 나옴 · 기간(start + days · UTC) 밖의 배너는 등용 불가.
/// </summary>
public static class GachaRules
{
@ -35,33 +39,114 @@ namespace TKS.OutGame
return g.CostSingle * count;
}
public static string CanDraw(OutGameData db, SaveData d, int count)
// ---------- 배너 ----------
/// <summary>배너 시작 시각(UTC · start 가 비면 null = 기간 없음).</summary>
public static DateTime? StartOf(GachaConfig.Banner b)
{
int cost = Cost(db, d, count);
if (d.gem < cost) return "보옥 부족 " + d.gem + " / " + cost;
if (Candidates(db, 1).Count == 0) return "뽑기 풀 비었음";
return null;
DateTime day;
if (b == null || string.IsNullOrEmpty(b.start)) return null;
if (!DateTime.TryParseExact(b.start, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out day)) return null;
return day;
}
/// <summary>풀에서 등급 tier 에 나오는 유닛(영웅 = 희귀도 = tier · 병사 = ★ 풀).</summary>
public static List<GachaConfig.PoolEntry> Candidates(OutGameData db, int tier)
/// <summary>배너 끝 시각(UTC · 기간 없음 = null).</summary>
public static DateTime? EndOf(GachaConfig.Banner b)
{
var s = StartOf(b);
if (s == null || b.days <= 0) return null;
return s.Value.AddDays(b.days);
}
/// <summary>지금 등용할 수 있는 배너인가(기간 없음 = 항상 · 기간 있음 = 시작 ≤ 지금 &lt; 끝).</summary>
public static bool IsOpen(GachaConfig.Banner b, DateTime nowUtc)
{
if (b == null) return false;
var s = StartOf(b);
if (s != null && nowUtc < s.Value) return false;
var e = EndOf(b);
return e == null || nowUtc < e.Value;
}
/// <summary>남은 기간(기간 없음·끝남 = 0).</summary>
public static TimeSpan Remaining(GachaConfig.Banner b, DateTime nowUtc)
{
var e = EndOf(b);
if (e == null || nowUtc >= e.Value) return TimeSpan.Zero;
return e.Value - nowUtc;
}
/// <summary>화면을 열 때 고르는 배너 = 표 순서에서 처음 열려 있는 것(없으면 첫 줄).</summary>
public static GachaConfig.Banner Default(OutGameData db, DateTime nowUtc)
{
foreach (var b in db.Gacha.banners)
if (IsOpen(b, nowUtc)) return b;
return db.Gacha.banners.Count > 0 ? db.Gacha.banners[0] : null;
}
/// <summary>어느 픽업 배너의 주요 영웅인가(pickup_exclusive 판정).</summary>
public static bool IsPickupHero(GachaConfig g, string heroId)
{
foreach (var b in g.banners)
if (b.IsPickup && b.featured.Contains(heroId)) return true;
return false;
}
/// <summary>그 유닛이 이 배너에서 나오는가(풀 등록 여부 in_pool 은 Candidates 가 본다).</summary>
public static bool InBanner(GachaConfig g, HeroDb heroes, GachaConfig.Banner b, HeroDb.Hero h)
{
if (b == null || h == null) return false;
if (h.isSoldier) return b.soldiers;
if (!b.pool.Contains(GachaConfig.PoolAll) && !b.pool.Contains(h.faction)) return false;
if (!b.IsPickup && g.PickupExclusive && IsPickupHero(g, h.id)) return false;
return true;
}
/// <summary>배너에서 등급 tier 에 나오는 유닛(영웅 = 희귀도 = tier · 병사 = ★ 풀).</summary>
public static List<GachaConfig.PoolEntry> Candidates(GachaConfig g, HeroDb heroes, GachaConfig.Banner b, int tier)
{
var res = new List<GachaConfig.PoolEntry>();
foreach (var p in db.Gacha.pool)
foreach (var p in g.pool)
{
if (!p.inPool) continue;
var h = db.Heroes.Get(p.heroId);
if (h == null) continue;
var h = heroes.Get(p.heroId);
if (h == null || !InBanner(g, heroes, b, h)) continue;
int t = h.isSoldier ? 1 : h.rarity;
if (t == tier) res.Add(p);
}
return res;
}
public static List<GachaPull> Draw(OutGameData db, SaveData d, int count, System.Random rng)
public static List<GachaConfig.PoolEntry> Candidates(OutGameData db, GachaConfig.Banner b, int tier)
{
return Candidates(db.Gacha, db.Heroes, b, tier);
}
// ---------- 천장 ----------
public static int Pity(SaveData d, GachaConfig.Banner b)
{
return b != null ? d.gacha.PityOf(b.pityGroup) : 0;
}
// ---------- 등용 ----------
public static string CanDraw(OutGameData db, SaveData d, GachaConfig.Banner b, int count, DateTime nowUtc)
{
if (b == null) return "등용 배너 없음";
if (!IsOpen(b, nowUtc)) return "등용 기간이 아닙니다";
int cost = Cost(db, d, count);
if (d.gem < cost) return "보옥 부족 " + d.gem + " / " + cost;
bool any = false;
for (int t = 1; t <= 3 && !any; t++) any = Candidates(db, b, t).Count > 0;
if (!any) return "등용 풀 비었음";
return null;
}
public static List<GachaPull> Draw(OutGameData db, SaveData d, GachaConfig.Banner b, int count, System.Random rng, DateTime nowUtc)
{
var res = new List<GachaPull>();
if (CanDraw(db, d, count) != null) return res;
if (CanDraw(db, d, b, count, nowUtc) != null) return res;
var g = db.Gacha;
if (count == g.TenCount && FirstTenFree(db, d)) d.gacha.firstTenUsed = true;
else d.gem -= Cost(db, d, count);
@ -70,7 +155,7 @@ namespace TKS.OutGame
bool guaranteed = false;
for (int i = 0; i < count; i++)
{
int n = d.gacha.pity + 1;
int n = d.gacha.PityOf(b.pityGroup) + 1;
int tier;
bool pity = n >= g.Pity;
if (pity) tier = 3;
@ -81,9 +166,9 @@ namespace TKS.OutGame
}
if (tier >= g.TenGuaranteeRarity) guaranteed = true;
if (count == g.TenCount && i == count - 1 && !guaranteed) tier = g.TenGuaranteeRarity;
d.gacha.pity = tier == 3 ? 0 : n;
d.gacha.SetPity(b.pityGroup, tier == 3 ? 0 : n);
d.gacha.total++;
var pull = Pick(db, tier, rng);
var pull = Pick(db, b, tier, rng);
if (pull == null) continue;
pull.pity = pity;
var h = db.Heroes.Get(pull.heroId);
@ -103,21 +188,28 @@ namespace TKS.OutGame
return res;
}
static GachaPull Pick(OutGameData db, int tier, System.Random rng)
static GachaPull Pick(OutGameData db, GachaConfig.Banner b, int tier, System.Random rng)
{
var list = Candidates(db, tier);
for (int t = tier; list.Count == 0 && t > 1; t--) list = Candidates(db, t - 1); // 그 등급 풀이 비면 아래 등급
// 그 등급 풀이 비면 아래 등급 · 아래도 없으면 위 등급(진영 풀은 등급이 빌 수 있음)
var list = Candidates(db, b, tier);
for (int t = tier - 1; list.Count == 0 && t >= 1; t--) list = Candidates(db, b, t);
for (int t = tier + 1; list.Count == 0 && t <= 3; t++) list = Candidates(db, b, t);
if (list.Count == 0) return null;
if (tier == 3)
bool pickup = false;
if (tier == 3 && b.IsPickup)
{
var pick = new List<GachaConfig.PoolEntry>();
var rest = new List<GachaConfig.PoolEntry>();
foreach (var p in list) (p.pickup ? pick : rest).Add(p);
if (pick.Count > 0 && (rest.Count == 0 || rng.NextDouble() < db.Gacha.PickupShare)) list = pick;
foreach (var p in list) (b.featured.Contains(p.heroId) ? pick : rest).Add(p);
if (pick.Count > 0 && (rest.Count == 0 || rng.NextDouble() < db.Gacha.PickupShare))
{
list = pick;
pickup = true;
}
else if (rest.Count > 0) list = rest;
}
var e = list[rng.Next(list.Count)];
return new GachaPull { heroId = e.heroId, tier = tier, pickup = e.pickup && tier == 3 };
return new GachaPull { heroId = e.heroId, tier = tier, pickup = pickup };
}
}
}

View File

@ -173,6 +173,18 @@ namespace TKS.OutGame
if (d.inv == null) d.inv = new Inventory();
if (d.adsUsed == null) d.adsUsed = new AdsUsed();
if (d.gacha == null) d.gacha = new GachaState();
if (d.gacha.pities == null) d.gacha.pities = new List<CountEntry>();
if (d.gacha.pity > 0)
{
// 구 세이브(배너 하나 시절)의 천장 → 상시 배너 묶음으로 옮김(PD 2026-11-13 배너 5 · 진행 보존)
foreach (var b in db.Gacha.banners)
if (!b.IsPickup)
{
d.gacha.SetPity(b.pityGroup, Mathf.Max(d.gacha.PityOf(b.pityGroup), d.gacha.pity));
break;
}
d.gacha.pity = 0;
}
if (d.shop == null) d.shop = new ShopState();
if (d.stages == null) d.stages = new List<StageState>();
// C 단계(PD #1093 C) — 구버전 세이브 = 비어 있음
@ -304,6 +316,24 @@ namespace TKS.OutGame
return p != null ? p.order : 1;
}
/// <summary>
/// 풀 성장 상태(명장 등용 배너 「상세 정보」 · PD 2026-11-13) = 최고 레벨 · 최고 등급 · 최고 ★ · 승급 장비 전부 장착(누적) · 스킬 = 그 상태의 상한.
/// 병사 유닛 = 최고 레벨만(승급·장비·진화 없음). 값은 전부 표(hero_levels · promotion · evolve · hero_gear · skills)에서 읽는다.
/// </summary>
public static HeroState FullGrowth(OutGameData db, HeroDb.Hero h)
{
var s = new HeroState { id = h.id, lv = db.Levels.MaxLevel, grade = db.Promotion.FirstGrade, star = Mathf.Clamp(h.rarity, 1, db.Evolve.MaxStar) };
if (h.isSoldier) return s;
PromotionTable.Row top = null;
foreach (var r in db.Promotion.rows)
if (top == null || r.order > top.order) top = r;
if (top != null) s.grade = top.grade;
s.star = db.Evolve.MaxStar;
foreach (var set in h.gear) s.gearTotal += set.slots.Count;
s.skillLv = new[] { SkillRules.Cap(db, h, s, 0), SkillRules.Cap(db, h, s, 1) };
return s;
}
// ---------- 진화 ----------
/// <summary>다음 ★ 에 필요한 조각(최고 ★ = 0).</summary>

View File

@ -83,7 +83,7 @@ namespace TKS.OutGame
var names = new List<string>();
if (pulls != null)
foreach (var p in pulls) names.Add(p.heroId + "★" + p.tier + (p.isNew ? "N" : p.shards > 0 ? "+" + p.shards : "+g" + p.gold));
Log("gacha 10 = " + string.Join(" ", names.ToArray()) + " · pity=" + s.Save.gacha.pity + " gem=" + s.Save.gem + " firstTenUsed=" + s.Save.gacha.firstTenUsed);
Log("gacha 10 = " + string.Join(" ", names.ToArray()) + " · pity=" + GachaRules.Pity(s.Save, root.Trade.Banner) + " gem=" + s.Save.gem + " firstTenUsed=" + s.Save.gacha.firstTenUsed);
yield return Shot(cam, "gacha_result");
root.Trade.CloseResult();
root.Trade.SetPage(2);

View File

@ -295,9 +295,9 @@ namespace TKS.OutGame
// ---------- 거래 ----------
public List<GachaPull> Draw(int count)
public List<GachaPull> Draw(GachaConfig.Banner banner, int count)
{
var res = GachaRules.Draw(Db, Save, count, Rng);
var res = GachaRules.Draw(Db, Save, banner, count, Rng, Now);
if (res.Count > 0) Commit();
return res;
}

View File

@ -94,6 +94,7 @@ namespace TKS.OutGame
"heroes.csv", "skills.csv", "hero_levels.csv", "promotion.csv", "evolve.csv", "gear_items.csv",
"hero_gear.csv", "skill_levels.csv", "gacha.csv", "gacha_pool.csv", "stage_rewards.csv", "economy.csv",
"skill_effects.csv", // 스킬 효과 1줄 표(2026-11-10 #1112)
"gacha_banners.csv", // 명장 등용 배너(픽업 1 + 국가별 상시 4 · PD 2026-11-13)
// C 단계(PD #1093 C)
"training.csv", "training_ranks.csv", "patrol.csv", "dev_tree.csv", "treasures.csv", "treasure_sets.csv", "chests.csv", "tower.csv", "dungeons.csv", "arena.csv",
};
@ -131,7 +132,7 @@ namespace TKS.OutGame
d.Gear = ParseGear(rows["gear_items.csv"], e);
d.Skills = ParseSkills(rows["skills.csv"], rows["skill_levels.csv"], rows["skill_effects.csv"], e);
d.Heroes = ParseHeroes(rows["heroes.csv"], rows["hero_gear.csv"], d, e);
d.Gacha = ParseGacha(rows["gacha.csv"], rows["gacha_pool.csv"], d, e);
d.Gacha = ParseGacha(rows["gacha.csv"], rows["gacha_pool.csv"], rows["gacha_banners.csv"], d, e);
d.Rewards = ParseRewards(rows["stage_rewards.csv"], d, e);
d.Economy = ParseEconomy(rows["economy.csv"], d, e);
CheckSkillOwners(d, e);
@ -410,7 +411,7 @@ namespace TKS.OutGame
return t;
}
static GachaConfig ParseGacha(List<Dictionary<string, string>> rows, List<Dictionary<string, string>> poolRows, OutGameData d, List<string> e)
static GachaConfig ParseGacha(List<Dictionary<string, string>> rows, List<Dictionary<string, string>> poolRows, List<Dictionary<string, string>> bannerRows, OutGameData d, List<string> e)
{
var t = ScriptableObject.CreateInstance<GachaConfig>();
KeyValues(rows, "gacha.csv", GachaConfig.RequiredKeys, t.values, e);
@ -420,18 +421,66 @@ namespace TKS.OutGame
if (Mathf.Abs(sum - 1f) > 1e-4f) e.Add("gacha.csv: p_r1 + p_r2 + p_r3 = " + sum.ToString("0.####", CultureInfo.InvariantCulture) + " ≠ 1");
}
const string f = "gacha_pool.csv";
if (!Cols(poolRows, f, e, "hero_id", "in_pool", "pickup")) return t;
if (!Cols(poolRows, f, e, "hero_id", "in_pool")) return t;
var seen = new HashSet<string>();
foreach (var r in poolRows)
{
var p = new GachaConfig.PoolEntry { heroId = Get(r, "hero_id"), inPool = Get(r, "in_pool") == "1", pickup = Get(r, "pickup") == "1" };
var p = new GachaConfig.PoolEntry { heroId = Get(r, "hero_id"), inPool = Get(r, "in_pool") == "1" };
if (!Unique(seen, p.heroId, f, r, e)) continue;
if (d.Heroes == null || d.Heroes.Get(p.heroId) == null) e.Add(f + " 행 " + Get(r, "#") + ": hero_id 「" + p.heroId + "」 가 heroes.csv 에 없음");
t.pool.Add(p);
}
ParseBanners(t, bannerRows, d, e);
return t;
}
/// <summary>
/// gacha_banners.csv(PD 2026-11-13 명장 등용) — 배너 1줄 = id · type(pickup|standing) · faction · 이름·설명 · pool(진영 목록 · all) · soldiers(0|1) ·
/// featured(주요 영웅 · 픽업 = 확률 상승 대상) · pity_group(천장 묶음) · start(yyyy-MM-dd UTC · 빈 칸 = 기간 없음) · days · art(배너 그림 테마 id).
/// 검증 = id 유일 · type · 주요 영웅이 heroes.csv 에 있고 그 배너 풀에서 나오는지 · 배너마다 등급 1~3 중 나올 유닛이 있는지 · 날짜 형식.
/// </summary>
static void ParseBanners(GachaConfig t, List<Dictionary<string, string>> rows, OutGameData d, List<string> e)
{
const string f = "gacha_banners.csv";
if (!Cols(rows, f, e, "banner_id", "type", "faction", "name_ko", "desc_ko", "pool", "soldiers", "featured", "pity_group", "start", "days", "art")) return;
var seen = new HashSet<string>();
var sep = new[] { ';' };
foreach (var r in rows)
{
var b = new GachaConfig.Banner
{
id = Get(r, "banner_id"), type = Get(r, "type"), faction = Get(r, "faction"), nameKo = Get(r, "name_ko"), descKo = Get(r, "desc_ko"),
soldiers = Get(r, "soldiers") == "1", pityGroup = Get(r, "pity_group"), start = Get(r, "start"), days = Int(r, "days", f, e), art = Get(r, "art"),
};
b.pool.AddRange(Get(r, "pool").Split(sep, StringSplitOptions.RemoveEmptyEntries));
b.featured.AddRange(Get(r, "featured").Split(sep, StringSplitOptions.RemoveEmptyEntries));
if (!Unique(seen, b.id, f, r, e)) continue;
string at = f + " 행 " + Get(r, "#") + " 「" + b.id + "」: ";
if (b.type != GachaConfig.TypePickup && b.type != GachaConfig.TypeStanding) e.Add(at + "type 「" + b.type + "」 = pickup | standing 만");
if (b.pool.Count == 0) e.Add(at + "pool 비었음(진영 목록 또는 all)");
if (b.pityGroup.Length == 0) e.Add(at + "pity_group 비었음");
if (b.featured.Count == 0) e.Add(at + "featured 비었음");
DateTime day;
if (b.start.Length > 0 && !DateTime.TryParseExact(b.start, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out day)) e.Add(at + "start 「" + b.start + "」 = yyyy-MM-dd");
if (b.days < 0) e.Add(at + "days < 0");
t.banners.Add(b);
}
if (t.banners.Count == 0) e.Add(f + ": 배너 없음");
if (d.Heroes == null) return;
foreach (var b in t.banners)
{
foreach (var id in b.featured)
{
var h = d.Heroes.Get(id);
if (h == null) e.Add(f + " 「" + b.id + "」: featured 「" + id + "」 가 heroes.csv 에 없음");
else if (!GachaRules.InBanner(t, d.Heroes, b, h)) e.Add(f + " 「" + b.id + "」: featured 「" + id + "」 가 이 배너 풀에서 나오지 않음(pool · gacha_pool in_pool · pickup_exclusive)");
}
bool any = false;
for (int tier = 1; tier <= 3 && !any; tier++) any = GachaRules.Candidates(t, d.Heroes, b, tier).Count > 0;
if (!any) e.Add(f + " 「" + b.id + "」: 나올 유닛이 없음");
}
}
static RewardTable ParseRewards(List<Dictionary<string, string>> rows, OutGameData d, List<string> e)
{
var t = ScriptableObject.CreateInstance<RewardTable>();

View File

@ -121,10 +121,23 @@ namespace TKS.OutGame
[Serializable]
public class GachaState
{
[Tooltip("마지막 ★★★ 이후 뽑은 횟수(천장 카운터)")]
[Tooltip("구 세이브(배너 하나 시절)의 천장 카운터 — 읽을 때 상시 배너 묶음으로 옮긴다(HeroRules.Sanitize)")]
public int pity;
public int total;
public bool firstTenUsed;
[Tooltip("천장 묶음(gacha_banners.csv pity_group)별 마지막 ★★★ 이후 등용 횟수")]
public List<CountEntry> pities = new List<CountEntry>();
public int PityOf(string group)
{
return Counts.Get(pities, group);
}
public void SetPity(string group, int n)
{
if (pities == null) pities = new List<CountEntry>();
Counts.Set(pities, group, n);
}
}
[Serializable]

View File

@ -1052,7 +1052,7 @@ namespace TKS.OutGame
{
var ribbon = UiKit.Rect("Title", ui, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -170f), new Vector2(620f, 130f));
if (UISkin.Has("ribbon_red")) UiKit.Skin(ribbon, "ribbon_red", UiKit.White);
UiKit.Title(UiKit.Label(ribbon, "뽑기 결과", 58, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 12f), new Vector2(520f, 90f)), true).raycastTarget = false;
UiKit.Title(UiKit.Label(ribbon, "등용 결과", 58, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 12f), new Vector2(520f, 90f)), true).raycastTarget = false;
// 카드마다 그 뽑기 직후의 조각 수(같은 영웅이 여러 번 나오면 차례로 쌓임 · 세이브에는 이미 다 더해져 있음)
var total = new Dictionary<string, int>();

View File

@ -0,0 +1,136 @@
using UnityEngine;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 영웅 상세 정보 팝업(PD 2026-11-13 명장 등용 「상세 정보를 누르면 해당 영웅의 풀 성장 스펙 및 스킬 정보를 볼 수 있는 팝업」) —
/// 머리(초상 · 이름 · 진영 · 병종 · ★(등급만 켜짐) · 전투력) → 풀 성장 능력치(HP · 공격력 · 방어력 = HeroRules.Compute(FullGrowth) · 비교 = 처음 얻었을 때) →
/// 스킬 2행(액티브 · 패시브 = 아이콘 · 이름 · 한자 · 쿨 · 최고 레벨 · 설명). 부품·색 = 영웅 상세 화면(HeroDetailScreen) 것 그대로 · 보유하지 않은 영웅도 열린다.
/// 값 = 표(hero_levels · promotion · evolve · hero_gear · skills) — 훈련·보물 효과는 유저마다 달라 넣지 않는다.
/// </summary>
public static class HeroInfoPopup
{
const float W = 860f; // 팝업 내용 칸 폭(ModalCustom 940 − 좌우 40)
const float RowIcon = 126f;
/// <summary>마지막으로 연 팝업의 풀 성장 수치(테스트·캡처 로그).</summary>
public static HeroRules.Stats LastStats { get; private set; }
public static string LastHeroId { get; private set; }
public static bool Show(OutGameRoot root, string heroId)
{
var s = root.Session;
var db = s.Db;
var h = s.Def(heroId);
if (h == null) return false;
var full = HeroRules.FullGrowth(db, h);
var st = HeroRules.Compute(db, s.Cfg, h, full);
var first = HeroRules.Compute(db, s.Cfg, h, HeroRules.NewHero(db, h, 0));
LastStats = st;
LastHeroId = heroId;
var c = root.ModalCustom("상세 정보", 1200f, new OutGameRoot.ModalButton("닫기", UiKit.Dark, null));
string g = h.isSoldier ? "white" : full.grade;
// ① 머리 = 초상(최고 등급 색 틀) · 이름 · 한자 · 진영 배지 · 병종 · ★ · 전투력
UiKit.Portrait("Face", c, h, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(0f, -4f), new Vector2(204f, 204f), false, 1f, UiKit.Col("grade_" + g + "_card_bg", UiKit.Coin));
var name = HeroDetailScreen.Txt(c, h.nameKo, 228f, 6f, 60, "white");
if (!string.IsNullOrEmpty(h.hanja)) HeroDetailScreen.Txt(c, h.hanja, 228f + name.preferredWidth + 14f, 32f, 30, "text_soft", TextAnchor.MiddleLeft, 0f, false, 0f, false);
UiKit.DropShadow(UiKit.FactionBadge(c, h.faction, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(228f, -88f), 64f), 3f);
HeroDetailScreen.Ico(c, UiKit.TypeSymbolId(h.type), 304f, 90f, 60f, 60f);
HeroDetailScreen.Txt(c, UnitTypeInfo.Korean(h.type), 372f, 104f, 30, "text_type", TextAnchor.MiddleLeft, 0f, false, 0f, false);
// 머리 별 = 그 영웅의 등급(배너·카드와 같은 수) · 풀 성장 ★ 까지 남은 칸 = 꺼진 별(QA r1 M6 「배너 ★3 ↔ 팝업 ★5」)
int stars = h.isSoldier ? h.rarity : full.star;
for (int i = 0; i < stars; i++) HeroDetailScreen.Ico(c, i < h.rarity ? "star_l" : "star_off", 228f + i * 50f, 160f, 48f, 50f, i < h.rarity ? 3f : 0f);
var pw = HeroDetailScreen.Spr(c, "Power", "chip", W - 316f, 150f, 316f, 66f, "chip_dark").transform;
HeroDetailScreen.Ico(pw, "sword", 14f, 4f, 58f, 58f);
HeroDetailScreen.Txt(pw, "전투력", 78f, 21f, 26, "text_soft", TextAnchor.MiddleLeft, 0f, true, 0f, false);
HeroDetailScreen.Txt(pw, st.power.ToString("N0"), 164f, 12f, 44, "white");
// ② 풀 성장 능력치 = 기준 줄 + 칸 3(큰 값 = 풀 성장 · 아래 = 처음 얻었을 때)
float y = 236f;
UiKit.SubTitle(c, "풀 성장 능력치", 0f, y, W, 34);
string basis = h.isSoldier
? "Lv " + full.lv + " 기준"
: "Lv " + full.lv + " · " + UiKit.GradeKo(full.grade) + " 등급 · ★" + full.star + " · 승급 장비 " + full.gearTotal + "개 기준";
HeroDetailScreen.Txt(c, basis, 0f, y + 52f, 26, "text_soft", TextAnchor.MiddleCenter, W, false, 0f, false);
y += 100f;
string[] names = { "HP", "공격력", "방어력" };
string[] icons = { "dom_stat_hp", "dom_stat_atk", "dom_stat_def" };
int[] now = { st.Hp, st.Atk, st.Def };
int[] was = { first.Hp, first.Atk, first.Def };
const float tw = 276f, th = 150f;
for (int i = 0; i < 3; i++)
{
var tile = HeroDetailScreen.Spr(c, "Stat" + i, "panel12", i * (tw + 16f), y, tw, th, "grade_" + g + "_row").transform;
HeroDetailScreen.Ico(tile, icons[i], 14f, 14f, 52f, 52f, 3f);
HeroDetailScreen.Txt(tile, names[i], 76f, 22f, 30, "white", TextAnchor.MiddleLeft, 0f, true);
HeroDetailScreen.Txt(tile, now[i].ToString("N0"), 0f, 62f, 48, "white", TextAnchor.MiddleCenter, tw);
HeroDetailScreen.Txt(tile, "처음 " + was[i].ToString("N0"), 0f, 116f, 22, "text_soft", TextAnchor.MiddleCenter, tw, false, 0f, false);
}
y += th + 14f;
HeroDetailScreen.Txt(c, "내정 훈련 · 보물 효과는 포함하지 않은 값", 0f, y, 22, "text_soft", TextAnchor.MiddleCenter, W, false, 0f, false);
y += 46f;
// ③ 스킬 2행(액티브 · 패시브) — 행 높이 = 설명 줄 수에 맞춤
UiKit.SubTitle(c, "스킬", 0f, y, W, 34);
y += 58f;
if (h.isSoldier)
{
HeroDetailScreen.Txt(c, "병사 유닛은 스킬이 없습니다", 0f, y + 10f, 28, "text_soft", TextAnchor.MiddleCenter, W, false, 0f, false);
y += 70f;
}
else
{
string tk = UiKit.TypeKey(h.type);
for (int i = 0; i < 2; i++)
{
var sk = SkillRules.Skill(db, h, i);
if (sk == null) continue;
var row = HeroDetailScreen.Spr(c, "Skill" + i, "panel12", 0f, y, W, 160f, "grade_" + g + "_row");
var rt = row.transform;
string iconId = "skill_" + sk.id;
bool hasIcon = UISkin.Theme != null && UISkin.Theme.IconEntryOf(iconId) != null;
// 아이콘 = 영웅 상세와 같은 규칙(256² 사각 아이콘 + 병종 얇은 틀 · 아이콘이 없으면 두꺼운 틀 + 병종 기호)
if (hasIcon)
{
HeroDetailScreen.Ico(rt, iconId, 14f + 10.34f, 16f + 10.34f, 105.33f, 105.33f);
if (tk != null) HeroDetailScreen.Ico(rt, "skill_ring_" + tk, 14f, 16f, RowIcon, RowIcon, 4f);
}
else
{
if (tk != null) HeroDetailScreen.Ico(rt, "skill_" + tk + "_a", 14f, 16f, RowIcon, RowIcon, 4f);
HeroDetailScreen.Ico(rt, UiKit.TypeSymbolId(h.type), 36f, 38f, 82f, 82f);
}
float x = 158f;
var kind = HeroDetailScreen.Spr(rt, "Kind", "chip", x, 14f, 116f, 44f, i == 0 ? "chip_cool" : "chip_dark");
HeroDetailScreen.Txt(kind.transform, i == 0 ? "액티브" : "패시브", 0f, 9f, 26, "white", TextAnchor.MiddleCenter, 116f, true, 3f);
if (i == 0 && sk.cooldown > 0)
{
var cool = HeroDetailScreen.Spr(rt, "Cool", "chip", x + 126f, 14f, 124f, 44f, "chip_dark");
HeroDetailScreen.Txt(cool.transform, "쿨 " + sk.cooldown + "턴", 0f, 9f, 26, "white", TextAnchor.MiddleCenter, 124f, false, 3f);
}
var lv = HeroDetailScreen.Spr(rt, "Lv", "chip", W - 176f, 14f, 160f, 44f, "chip_dark");
HeroDetailScreen.Txt(lv.transform, "최고 Lv " + SkillRules.Level(full, i), 0f, 9f, 26, "white", TextAnchor.MiddleCenter, 160f, true, 3f);
var nm = HeroDetailScreen.Txt(rt, sk.nameKo, x, 66f, 38, "white");
if (!string.IsNullOrEmpty(sk.hanja)) HeroDetailScreen.Txt(rt, sk.hanja, x + nm.preferredWidth + 10f, 76f, 26, "grade_" + g + "_label", TextAnchor.MiddleLeft, 0f, false, 0f, false);
var desc = HeroDetailScreen.Desc(rt, x, 118f, W - x - 20f);
desc.fontSize = 24;
desc.text = SkillInfoDb.DisplayDesc(sk);
desc.verticalOverflow = VerticalWrapMode.Overflow;
float dh = Mathf.Ceil(desc.preferredHeight);
desc.rectTransform.sizeDelta = new Vector2(desc.rectTransform.sizeDelta.x, dh);
float rh = Mathf.Max(160f, 118f + dh + 18f);
row.rectTransform.sizeDelta = new Vector2(W, rh);
y += rh + 14f;
}
}
// 팝업 높이 = 내용에 맞춤(제목 띠 130 + 아래 버튼 줄 160)
var panel = (RectTransform)c.parent;
panel.sizeDelta = new Vector2(panel.sizeDelta.x, Mathf.Clamp(y + 300f, 600f, 1620f));
return true;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f2d86e42625cf4f4cbb08170129582ee

View File

@ -210,7 +210,7 @@ namespace TKS.OutGame
public static string InfoText(HeroDb.Hero h, bool own)
{
return h.nameKo + (string.IsNullOrEmpty(h.hanja) ? "" : "(" + h.hanja + ")") + " · " + UnitTypeInfo.Korean(h.type) + " · ★" + h.rarity +
(h.isSoldier ? " · 병사 유닛" : "") + (own ? " · 보유" : " · 미보유 — 거래 > 뽑기");
(h.isSoldier ? " · 병사 유닛" : "") + (own ? " · 보유" : " · 미보유 — 거래 > 명장 등용");
}
public static Image EmptyIn(Transform holder, float k)
@ -453,19 +453,19 @@ namespace TKS.OutGame
foreach (var o in S.Owned(h.isSoldier)) order.Add(o.id);
}
// ---------- 목업 좌표 도우미(부모 왼쪽 위 기준 · y 아래로) ----------
// ---------- 목업 좌표 도우미(부모 왼쪽 위 기준 · y 아래로) · internal = 영웅 정보 팝업(HeroInfoPopup)이 같은 부품·색으로 그림 ----------
static Color Col(string colorId)
internal static Color Col(string colorId)
{
return UISkin.Col(colorId, Color.white);
}
static RectTransform At(Transform p, string name, float x, float y, float w, float h)
internal static RectTransform At(Transform p, string name, float x, float y, float w, float h)
{
return UiKit.Rect(name, p, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(x, -y), new Vector2(w, h));
}
static Image Spr(Transform p, string name, string skin, float x, float y, float w, float h, string color, bool raycast = false)
internal static Image Spr(Transform p, string name, string skin, float x, float y, float w, float h, string color, bool raycast = false)
{
return UiKit.Skin(At(p, name, x, y, w, h), skin, Col(color), raycast);
}
@ -477,7 +477,7 @@ namespace TKS.OutGame
return img;
}
static Image Ico(Transform p, string icon, float x, float y, float w, float h, float shadow = 0f)
internal static Image Ico(Transform p, string icon, float x, float y, float w, float h, float shadow = 0f)
{
var img = At(p, "Icon", x, y, w, h).gameObject.AddComponent<Image>();
img.preserveAspect = true;
@ -488,7 +488,7 @@ namespace TKS.OutGame
}
/// <summary>테마 아이콘(팩 임시 = 표 tint · 신규 제작 = 원색) · alpha = 곱.</summary>
static void SetIcon(Image img, string icon, float alpha)
internal static void SetIcon(Image img, string icon, float alpha)
{
var e = UISkin.Theme != null && icon != null ? UISkin.Theme.IconEntryOf(icon) : null;
img.sprite = e != null ? e.Sprite : null;
@ -506,7 +506,7 @@ namespace TKS.OutGame
}
/// <summary>목업 text() — y = 글자 윗변 · title = Sen(+ 한글 폴백) · false = 맑은 고딕 · stroke &lt; 0 = 기본(크기 × 0.06) · 0 = 없음 · 그 밖 = 목업 px(절반).</summary>
static Text Txt(Transform p, string s, float x, float y, int size, string color, TextAnchor align = TextAnchor.MiddleLeft, float w = 0f, bool title = true, float stroke = -1f, bool shadow = true)
internal static Text Txt(Transform p, string s, float x, float y, int size, string color, TextAnchor align = TextAnchor.MiddleLeft, float w = 0f, bool title = true, float stroke = -1f, bool shadow = true)
{
float bw = w > 0f ? w : 900f;
float px = align == TextAnchor.MiddleCenter ? 0.5f : align == TextAnchor.MiddleRight ? 1f : 0f;
@ -532,7 +532,7 @@ namespace TKS.OutGame
return t;
}
static Text Desc(Transform p, float x, float y, float w)
internal static Text Desc(Transform p, float x, float y, float w)
{
var t = Txt(p, "", x, y, 26, "text_desc", TextAnchor.UpperLeft, w, false, 0f, false);
var rt = t.rectTransform;

View File

@ -1,3 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
@ -6,10 +7,54 @@ using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 거래(img11 · 기획 S15) — ① 영웅·유닛 뽑기(픽업 · 천장 n/80 · 확률 공개 · 1회/10연 · 첫 10연 무료 · 결과 = 카드 순차 공개)
/// 거래(img11 · 기획 S15) — ① 명장 등용(뽑기 · PD 2026-11-13: 배너 2쪽 = 픽업(영웅 1명 강조 일러 · 기간 · 상세 정보) / 국가별 상시(지도 위 진영 깃발 4 = 위·촉·오·군 · 주요 영웅 5) ·
/// 좌우 화살표로 쪽 넘김 · 천장 n/80 · 확률 공개 · 등용 1회/10회 · 첫 10회 무료 · 결과 = 삼고초려 연출 → 카드)
/// ② 보물 상자(PD #1093 C) = 상자 5종 · 보유 수 · 구매(chests.csv buy_gold/buy_gem/buy_points · 거래 할인 개발) · 열기는 보물 > 상자 탭
/// ③ 상점·패스 = 골드 승급 장비 상점(흰/녹 · 일일 · 새로고침 보옥 · 시장 개설 무료) · 성장 패스·월정액·챕터 팩·첫 결제 팩 = 「준비 중」 스텁(과금 없음).
/// </summary>
/// <summary>
/// 명장 등용 배너 배치 표(Resources/UI/recruit_banner.csv · 형식 = 로비 장면 표 LobbyScene.Load) — 지도·깃발·영웅 자리·이름표 좌표 = 배너 그림 px.
/// 누가 나오는지·이름·기간은 OutGame 표 gacha_banners.csv(GachaConfig.Banner).
/// </summary>
public static class RecruitBanner
{
public const string TablePath = "UI/recruit_banner";
static List<LobbyScene.Row> rows;
public static void Reload()
{
rows = null;
}
public static List<LobbyScene.Row> Rows
{
get
{
if (rows == null) rows = LobbyScene.Load(TablePath);
return rows;
}
}
public static LobbyScene.Row First(string kind)
{
return DomesticScene.First(Rows, kind);
}
/// <summary>인원 n 의 자리 줄(i_slot 순) — 그 인원 줄이 없으면 n 보다 큰 가장 작은 인원 줄.</summary>
public static List<LobbyScene.Row> Slots(string kind, int n)
{
int best = int.MaxValue;
foreach (var r in Rows)
if (r.kind == kind && r.n >= n && r.n < best) best = r.n;
var list = new List<LobbyScene.Row>();
foreach (var r in Rows)
if (r.kind == kind && r.n == best) list.Add(r);
list.Sort((a, b) => a.slot.CompareTo(b.slot));
return list;
}
}
public class TradeScreen : OutGameScreen
{
int page;
@ -24,6 +69,41 @@ namespace TKS.OutGame
Button tenBtn;
RectTransform pickupArt;
// 명장 등용 배너(PD 2026-11-13) — 쪽 = 픽업 배너 1개씩 + 상시 배너 묶음 1쪽(진영 깃발로 고름)
GachaConfig.Banner banner;
string standingId;
readonly List<List<GachaConfig.Banner>> bannerPages = new List<List<GachaConfig.Banner>>();
RectTransform bannerPx;
RectTransform bannerDots;
Text pityNote;
readonly Text[] probNote = new Text[4];
/// <summary>지금 고른 등용 배너.</summary>
public GachaConfig.Banner Banner
{
get { return banner; }
}
public int BannerPageCount
{
get { return bannerPages.Count; }
}
/// <summary>지금 배너가 있는 쪽(0부터).</summary>
public int BannerPage
{
get
{
for (int i = 0; i < bannerPages.Count; i++)
if (bannerPages[i].Contains(banner)) return i;
return 0;
}
}
/// <summary>배너에 세운 주요 영웅 수 · 진영 깃발 버튼 수(테스트·캡처).</summary>
public int FeaturedShown { get; private set; }
public int FlagCount { get; private set; }
RectTransform shopGrid;
ScrollRect shopScroll;
Text shopTimer;
@ -75,9 +155,9 @@ namespace TKS.OutGame
{
Tab = 4;
if (UISkin.On) UiKit.Paint(UiKit.Fill("ListBg", Rt), UiKit.Col("list_bg", UiKit.Bg), false); // 목록형 바탕(Fable §5-3)
Header("거래", "뽑기 · 상자 · 상점 · 패스", true);
Header("거래", "등용 · 상자 · 상점 · 패스", true);
var body = Body();
string[] names = { "영웅·유닛 뽑기", "보물 상자", "상점·패스" };
string[] names = { "명장 등용", "보물 상자", "상점·패스" };
for (int i = 0; i < names.Length; i++)
{
int k = i;
@ -86,57 +166,78 @@ namespace TKS.OutGame
: UiKit.Btn("Tab" + i, body, names[i], 32, 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));
}
BuildBannerPages();
banner = GachaRules.Default(S.Db, S.Now);
if (UISkin.Has("panel_field") && UISkin.Has("btn_l_yellow")) BuildGachaKit(pages[0]);
else BuildGacha(pages[0]);
BuildShop(pages[2]);
}
// ---------- 뽑기(Fable 제안서 §4-4 · #1104) ----------
// ---------- 명장 등용(뽑기 · Fable 제안서 §4-4 · #1104 → PD 2026-11-13 배너 개편) ----------
const float GaugeW = 820f;
const float BannerW = 1032f, BannerH = 640f, BannerInset = 8f;
Text pityBadge;
Text tenCost;
Text singleCost;
void BuildBannerPages()
{
bannerPages.Clear();
List<GachaConfig.Banner> standing = null;
foreach (var b in S.Db.Gacha.banners)
{
if (b.IsPickup)
{
bannerPages.Add(new List<GachaConfig.Banner> { b });
continue;
}
if (standing == null)
{
standing = new List<GachaConfig.Banner>();
bannerPages.Add(standing);
}
standing.Add(b);
}
}
/// <summary>
/// 뽑기 = 배너(틀 유지 · 아래 문양 띠 흰 8 % · 오른쪽 위 「픽업」 뱃지) → 천장 섹션(레벨형 게이지 + 노랑 배지) →
/// 확률 섹션(제목 띠 · 행 3 = 별 아이콘 ×n + 바 + % Sen) → 1회(Secondary 하늘 + 보옥 비용) · 10연(Primary 노랑 + 「첫 무료」 꼬리 칩) · 각주 = ⓘ 팝업.
/// 명장 등용 = 배너(틀 + 안쪽 그림 · 좌우 화살표 = 쪽 넘김 · 아래 쪽 점) → 천장 섹션(레벨형 게이지 + 노랑 배지 · 배너 묶음) →
/// 확률 섹션(제목 띠 · 행 3 = 별 아이콘 ×n + 바 + % + 배너별 메모) → 등용 1회(Secondary 하늘 + 보옥 비용) · 10회(Primary 노랑 + 「첫 무료」 꼬리 칩) · 각주 = ⓘ 팝업.
/// 배너 안쪽 내용은 고를 때마다 다시 그린다(DrawBanner · 좌표 = 표 recruit_banner.csv).
/// </summary>
void BuildGachaKit(RectTransform p)
{
var g = S.Db.Gacha;
// 배너
var banner = UiKit.At(p, "Banner", 24f, 12f, 1032f, 600f);
UiKit.Skin(banner, "banner", Color.white);
if (UISkin.Has("banner_pattern"))
// 배너 = 틀 + 안쪽(둥근 마스크 · 그림 px 좌표계)
var frame = UiKit.At(p, "Banner", 24f, 12f, BannerW, BannerH);
UiKit.Skin(frame, "banner", Color.white);
var clip = UiKit.Fill("Clip", frame, BannerInset, BannerInset, BannerInset, BannerInset);
UiKit.Skin(clip, "panel_field", Color.white);
clip.gameObject.AddComponent<Mask>().showMaskGraphic = false;
var view = RecruitBanner.First("view");
float aw = BannerW - 2f * BannerInset, ah = BannerH - 2f * BannerInset;
float vw = view != null && view.w > 1f ? view.w : aw, vh = view != null && view.h > 1f ? view.h : ah;
bannerPx = UiKit.At(clip, "Px", 0f, 0f, vw, vh);
bannerPx.localScale = new Vector3(aw / vw, aw / vw, 1f);
if (bannerPages.Count > 1)
{
var pat = UiKit.Skin(UiKit.At(banner, "Pattern", 18f, 600f - 112f, 1032f - 36f, 90f), "banner_pattern", UiKit.Col("banner_pattern", Color.clear));
pat.type = Image.Type.Tiled;
}
UiKit.InfoChip(banner, "Period", 30f, 26f, 180f, 52f, null, "기간 2주", 28); // 「픽업」 = 오른쪽 위 뱃지 하나만(검수 결함 3)
UiKit.Title(UiKit.Label(banner, "영웅·유닛 뽑기", 60, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(36f, -100f), new Vector2(600f, 90f)), true);
UiKit.Label(banner, "모든 영웅·병사 유닛\n= 뽑기로 획득", 32, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.UpperLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(36f, -200f), new Vector2(520f, 110f), false);
var free = UiKit.Skin(UiKit.At(banner, "Free", 36f, 600f - 104f, 300f, 64f), "tail", UiKit.Col("tail_red", UiKit.Red));
UiKit.Title(UiKit.FillLabel(free.transform, "첫 10연 무료", 30, UiKit.White), true);
freeChip = free.gameObject;
pickupArt = UiKit.Rect("PickupArt", banner, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-20f, 20f), new Vector2(560f, 560f));
var hot = UiKit.Icon(banner, "badge_hot", new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), new Vector2(-52f, -44f), 88f); // 배너 안쪽(검수 결함 3)
if (hot != null)
{
UiKit.DropShadow(hot, 4f);
UiKit.Title(UiKit.FillLabel(hot.transform, "픽업", 24, UiKit.White), true);
BannerArrow(frame, "Prev", "arrow_prev", false);
BannerArrow(frame, "Next", "arrow_next", true);
}
bannerDots = UiKit.At(p, "Dots", 24f, 12f + BannerH + 3f, BannerW, 18f);
// 천장 게이지
RectTransform pr;
var pf = UiKit.SectionPanel(p, "Pity", 24f, 632f, 1032f, 176f, false, false, out pr);
UiKit.Title(UiKit.Label(pf, "천장", 34, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(26f, -12f), new Vector2(200f, 50f)), true);
pityText = UiKit.Label(pf, "", 28, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-26f, -12f), new Vector2(620f, 50f), false);
var pf = UiKit.SectionPanel(p, "Pity", 24f, 676f, 1032f, 176f, false, false, out pr);
UiKit.Title(UiKit.Label(pf, "천장", 34, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(26f, -12f), new Vector2(100f, 50f)), true);
pityNote = UiKit.Label(pf, "", 24, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(120f, -14f), new Vector2(300f, 46f), false);
pityText = UiKit.Label(pf, "", 28, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-26f, -12f), new Vector2(560f, 50f), false);
pityBar = UiKit.Gauge(pf, "Gauge", 20f, 62f, GaugeW + 60f, "lv_badge_y", "lv_fill", out pityBadge);
// 확률 공개
RectTransform probRoot;
var prob = UiKit.SectionPanel(p, "Prob", 24f, 828f, 1032f, 400f, false, false, out probRoot);
var prob = UiKit.SectionPanel(p, "Prob", 24f, 864f, 1032f, 384f, false, false, out probRoot);
UiKit.SectionTitle(probRoot, "확률 공개");
var info = UiKit.Rect("Info", probRoot, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-16f, -12f), new Vector2(64f, 64f));
UiKit.Skin(info, "badge_circle", UiKit.Col("chip_dark", UiKit.Dark), true);
@ -145,38 +246,385 @@ namespace TKS.OutGame
info.gameObject.AddComponent<Button>().onClick.AddListener(ShowGachaRules);
for (int t = 3; t >= 1; t--)
{
float y = 88f + (3 - t) * 98f;
float y = 84f + (3 - t) * 94f;
var row = UiKit.Skin(UiKit.At(prob, "Row" + t, 20f, y, 1032f - 12f - 40f, 80f), "panel12", UiKit.Col("row_on_field", UiKit.Paper)).transform;
for (int s = 0; s < t; s++)
{
var st = UiKit.Icon(row, "star_on", new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(40f + s * 40f, 0f), 40f);
if (st != null) UiKit.DropShadow(st, 2f);
}
var fill = UiKit.Bar("P" + t, row, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(180f, 0f), new Vector2(500f, 30f), UiKit.Slot, t == 3 ? UiKit.Col("grade_orange_card_bg", UiKit.Coin) : t == 2 ? UiKit.Col("grade_purple_card_bg", UiKit.Purple) : UiKit.Col("grade_blue_card_bg", UiKit.Jade));
var fill = UiKit.Bar("P" + t, row, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(180f, 0f), new Vector2(460f, 30f), UiKit.Slot, t == 3 ? UiKit.Col("grade_orange_card_bg", UiKit.Coin) : t == 2 ? UiKit.Col("grade_purple_card_bg", UiKit.Purple) : UiKit.Col("grade_blue_card_bg", UiKit.Jade));
UiKit.SetBar(fill, g.P(t));
var pt = UiKit.Title(UiKit.Label(row, (g.P(t) * 100f).ToString("0.#") + "%", 32, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(700f, 0f), new Vector2(140f, 50f)), true);
var pt = UiKit.Title(UiKit.Label(row, (g.P(t) * 100f).ToString("0.#") + "%", 32, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(660f, 0f), new Vector2(120f, 50f)), true);
pt.horizontalOverflow = HorizontalWrapMode.Overflow;
if (t == 1) UiKit.Label(row, "병사 포함", 24, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-20f, 0f), new Vector2(140f, 40f), false);
probNote[t] = UiKit.Label(row, "", 24, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-20f, 0f), new Vector2(200f, 40f), false);
probNote[t].horizontalOverflow = HorizontalWrapMode.Overflow;
}
// 1회 · 10연
singleBtn = UiKit.SkinBtn(p, "Single", "btn_sky", UiKit.White, "1회", 42, UiKit.White, 24f, 1288f, 420f, 116f, () => DoDraw(1)); // 버튼 행 = 탭 바 위 24 고정(검수 결함 4)
singleCost = UiKit.BtnCost(singleBtn, "cur_gem", "", 44f, 36);
tenBtn = UiKit.SkinBtn(p, "Ten", "btn_l_yellow", UiKit.White, g.TenCount + "연", 46, UiKit.Col("text_on_yellow", UiKit.Ink), 1056f - 580f, 1282f, 580f, 122f, () => DoDraw(g.TenCount));
// 등용 1회 · 10회
singleBtn = UiKit.SkinBtn(p, "Single", "btn_sky", UiKit.White, "1회 등용", 38, UiKit.White, 24f, 1276f, 420f, 116f, () => DoDraw(1)); // 버튼 행 = 탭 바 위 24 고정(검수 결함 4)
singleCost = UiKit.BtnCost(singleBtn, "cur_gem", "", 44f, 34);
tenBtn = UiKit.SkinBtn(p, "Ten", "btn_l_yellow", UiKit.White, g.TenCount + "회 등용", 44, UiKit.Col("text_on_yellow", UiKit.Ink), 1056f - 580f, 1270f, 580f, 122f, () => DoDraw(g.TenCount));
tenCost = UiKit.BtnCost(tenBtn, "cur_gem", "", 44f, 36);
var f2 = UiKit.Skin(UiKit.Rect("Free2", tenBtn.transform, new Vector2(1f, 1f), new Vector2(1f, 0.5f), new Vector2(-14f, 2f), new Vector2(200f, 50f)), "tail", UiKit.Col("tail_red", UiKit.Red));
UiKit.Title(UiKit.FillLabel(f2.transform, "첫 무료", 26, UiKit.White), true);
freeChip2 = f2.gameObject;
}
/// <summary>뽑기 규칙(구 각주 → ⓘ 팝업 · Fable §4-4).</summary>
/// <summary>배너 좌우 화살표(쪽 넘김 · 틀 가장자리에 걸침 · 어두운 원 바탕 + 화살표).</summary>
void BannerArrow(Transform frame, string name, string icon, bool right)
{
var side = new Vector2(right ? 1f : 0f, 0.5f);
var hit = UiKit.Rect(name, frame, side, side, new Vector2(right ? -4f : 4f, 0f), new Vector2(68f, 124f));
hit.gameObject.AddComponent<Image>().color = Color.clear;
hit.gameObject.AddComponent<Button>().onClick.AddListener(() => StepBanner(right ? 1 : -1));
var c = new Vector2(0.5f, 0.5f);
UiKit.Skin(UiKit.Rect("Bg", hit, c, c, Vector2.zero, new Vector2(60f, 60f)), "badge_circle", UiKit.Col("chip_dark", UiKit.Dark));
var ic = UiKit.Icon(hit, icon, c, c, new Vector2(right ? 2f : -2f, 0f), 38f);
if (ic != null) ic.preserveAspect = true;
else UiKit.Title(UiKit.FillLabel(hit, right ? ">" : "<", 40, UiKit.White), true);
}
/// <summary>배너 고르기(진영 깃발 · 캡처·테스트) — 상시 배너면 그 진영을 기억한다(쪽을 넘겼다 돌아와도 유지).</summary>
public void SelectBanner(string id)
{
var b = S.Db.Gacha.Get(id);
if (b == null) return;
banner = b;
if (!b.IsPickup) standingId = b.id;
Refresh();
}
/// <summary>좌우 화살표 = 쪽 넘김(끝에서 처음으로 돈다).</summary>
public void StepBanner(int d)
{
int n = bannerPages.Count;
if (n == 0) return;
var page = bannerPages[((BannerPage + d) % n + n) % n];
var pick = page[0];
foreach (var b in page)
if (b.id == standingId) pick = b;
SelectBanner(pick.id);
}
/// <summary>「상세 정보」 = 이 배너 주요 영웅(픽업 영웅)의 풀 성장 스펙·스킬 팝업.</summary>
public void ShowBannerInfo()
{
if (banner != null && banner.featured.Count > 0) ShowHeroInfo(banner.featured[0]);
}
public bool ShowHeroInfo(string heroId)
{
return HeroInfoPopup.Show(Root, heroId);
}
// ---------- 배너 안쪽(고를 때마다 다시 그림 · 좌표 = 표 recruit_banner.csv 의 그림 px) ----------
static readonly Vector2 TL = new Vector2(0f, 1f);
void DrawBanner()
{
UiKit.Clear(bannerPx);
FeaturedShown = FlagCount = 0;
var bg = UiKit.Fill("ArtBg", bannerPx).gameObject.AddComponent<Image>();
bg.raycastTarget = false;
bg.sprite = UISkin.Icon(banner.art);
if (bg.sprite == null) bg.color = UiKit.Col("panel_field", UiKit.Dark);
if (banner.IsPickup) DrawPickup();
else DrawStanding();
UiKit.Clear(bannerDots);
if (bannerPages.Count < 2) return;
int at = BannerPage;
float dx = -((bannerPages.Count - 1) * 30f + 22f) / 2f;
for (int i = 0; i < bannerPages.Count; i++)
{
// 지금 쪽 = 긴 흰 알약 · 나머지 = 옅은 점(QA r1 M5 「쪽 점이 작고 흐림」)
float w = i == at ? 40f : 18f;
var dot = UiKit.Rect("Dot" + i, bannerDots, new Vector2(0.5f, 0.5f), new Vector2(0f, 0.5f), new Vector2(dx, 0f), new Vector2(w, 18f));
UiKit.Skin(dot, "chip", i == at ? UiKit.White : UiKit.Col("text_soft", UiKit.Disabled));
dx += w + 12f;
}
}
/// <summary>
/// 국가별 상시 배너(PD 2026-11-13 참고 1 + 추가 지시): 제목 줄 · 왼쪽 = 중국 대륙 지도 두루마리 위에 진영 깃발 4(마름모꼴 · ↖위 ↙촉 ↗군 ↘오 = 버튼) ·
/// 오른쪽 = 그 진영 주요 영웅 5명이 한 무리로 서고 저마다 세로 이름표(영웅을 누르면 상세 정보).
/// </summary>
void DrawStanding()
{
var px = bannerPx;
UiKit.InfoChip(px, "Kind", 22f, 20f, 150f, 50f, null, "상시 등용", 26);
var title = UiKit.Title(UiKit.Label(px, banner.nameKo, 54, UiKit.White, TextAnchor.MiddleLeft, TL, TL, new Vector2(188f, -8f), new Vector2(520f, 76f)), true);
title.horizontalOverflow = HorizontalWrapMode.Overflow;
var sub = UiKit.Title(UiKit.Label(px, banner.descKo, 24, UiKit.White, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -26f), new Vector2(430f, 40f)), true);
sub.horizontalOverflow = HorizontalWrapMode.Overflow;
var m = RecruitBanner.First("map");
if (m != null)
{
var map = UiKit.At(px, "Map", m.x, m.y, m.w, m.h);
var mi = map.gameObject.AddComponent<Image>();
mi.sprite = UISkin.Icon(m.sprite);
mi.raycastTarget = false;
mi.enabled = mi.sprite != null;
var glow = RecruitBanner.First("flag_glow");
var page = bannerPages[BannerPage];
// 고르지 않은 깃발 먼저 · 고른 깃발은 맨 위(크게 · 밑에 맥동하는 금빛)
for (int pass = 0; pass < 2; pass++)
foreach (var b in page)
{
bool on = b == banner;
if (on != (pass == 1)) continue;
var r = RecruitBanner.First("flag_" + b.faction);
if (r == null) continue;
var at = new Vector2(r.x, -r.y);
var c = new Vector2(0.5f, 0.5f);
if (on && glow != null && UISkin.Has(glow.sprite))
{
var gl = UiKit.Skin(UiKit.Rect("FlagGlow", map, TL, c, at, new Vector2(glow.w, glow.w)), glow.sprite, glow.color);
var pulse = gl.gameObject.AddComponent<UiPulse>();
pulse.min = glow.min;
pulse.max = glow.max;
pulse.period = glow.period;
}
float size = r.w * (on ? r.max : r.min);
var f = UiKit.Rect("Flag_" + b.id, map, TL, c, at, new Vector2(size, size));
var fi = f.gameObject.AddComponent<Image>();
fi.sprite = UISkin.Icon(r.sprite);
fi.preserveAspect = true;
float sh = on || r.shade < 0f ? 1f : r.shade;
fi.color = fi.sprite != null ? new Color(sh, sh, sh, 1f) : Color.clear;
if (fi.sprite != null) UiKit.DropShadow(fi, 4f);
else UiKit.FactionBadge(f, b.faction, c, c, Vector2.zero, size * 0.8f);
string id = b.id;
f.gameObject.AddComponent<Button>().onClick.AddListener(() => SelectBanner(id));
FlagCount++;
}
UiKit.InfoChip(px, "Hint", m.x + 30f, m.y + m.h + 10f, m.w - 60f, 46f, null, "깃발을 눌러 진영 선택", 24);
int heroes, soldiers;
CountPool(banner, out heroes, out soldiers);
UiKit.InfoChip(px, "Pool", m.x + 6f, m.y + m.h + 62f, m.w - 12f, 42f, null, UiKit.FactionKo(banner.faction) + " 진영 영웅 " + heroes + "명" + (soldiers > 0 ? " · 병사 " + soldiers + "종" : "") + " 등장", 22);
}
DrawFeatured();
}
/// <summary>이 배너에서 나오는 영웅 수 · 병사 유닛 종 수(풀 표 기준).</summary>
void CountPool(GachaConfig.Banner b, out int heroes, out int soldiers)
{
heroes = soldiers = 0;
for (int t = 1; t <= 3; t++)
foreach (var e in GachaRules.Candidates(S.Db, b, t))
{
var h = S.Def(e.heroId);
if (h != null && h.isSoldier) soldiers++;
else heroes++;
}
}
/// <summary>주요 영웅 무리(표 hero 줄 = 발 자리 · 뒤 → 앞 순서로 그림) + 세로 이름표(표 tag 줄) · 영웅 누름 = 상세 정보.</summary>
void DrawFeatured()
{
var px = bannerPx;
int n = banner.featured.Count;
var order = RecruitBanner.Slots("hero", n);
order.Sort((a, b) => a.y.CompareTo(b.y));
var hit = RecruitBanner.First("hit");
var shadow = RecruitBanner.First("shadow");
foreach (var r in order)
{
var h = r.slot < n ? S.Def(banner.featured[r.slot]) : null;
if (h == null) continue;
var foot = new Vector2(r.x, -r.y);
if (shadow != null && UISkin.Has(shadow.sprite))
UiKit.Skin(UiKit.Rect("Shadow", px, TL, new Vector2(0.5f, 0.5f), foot, new Vector2(r.h * shadow.w, r.h * shadow.h)), shadow.sprite, shadow.color);
if (UiKit.UnitStand("Hero_" + h.id, px, h, TL, foot, r.h, r.max) == null)
UiKit.Portrait("Hero_" + h.id, px, h, TL, new Vector2(0.5f, 0f), foot, new Vector2(r.h, r.h), false, 1f, Color.clear);
FeaturedShown++;
var hb = UiKit.Rect("Hit_" + h.id, px, TL, new Vector2(0.5f, 0f), foot, new Vector2(hit != null ? hit.w : r.h, hit != null ? hit.h : r.h));
hb.gameObject.AddComponent<Image>().color = Color.clear;
string id = h.id;
hb.gameObject.AddComponent<Button>().onClick.AddListener(() => ShowHeroInfo(id));
}
foreach (var r in RecruitBanner.Slots("tag", n))
{
var h = r.slot < n ? S.Def(banner.featured[r.slot]) : null;
if (h != null) NameTagV(px, r, h.nameKo);
}
}
/// <summary>
/// 세로 이름표(PD 참고 1 = 영웅마다 세로 이름표) — 표 tag 줄: 목패(가로 그림을 세워서 씀 · 9 분할 유지) · 판 색 · 글자 = 한 자씩 세로로.
/// f_x = 가운데 · f_y = 윗변(i_count 1 = 아랫변 · 머리 바로 위에 붙이는 이름표) · f_w 폭 · f_h 글자 1자 높이 · f_scale 글자 크기 · f_min 위아래 여백.
/// </summary>
static void NameTagV(Transform px, LobbyScene.Row r, string name)
{
float w = r.w > 1f ? r.w : 46f, ch = r.h > 1f ? r.h : 30f, pad = r.min > 1f ? r.min : 12f;
int fs = r.scale > 1f ? Mathf.RoundToInt(r.scale) : 26;
float h = name.Length * ch + pad * 2f;
var box = UiKit.Rect("Tag", px, TL, new Vector2(0.5f, r.count == 1 ? 0f : 1f), new Vector2(r.x, -r.y), new Vector2(w, h)); // i_count 1 = f_y 가 아랫변
var c = new Vector2(0.5f, 0.5f);
var plate = UiKit.Rect("Plate", box, c, c, Vector2.zero, new Vector2(h, w));
plate.localEulerAngles = new Vector3(0f, 0f, 90f);
var img = plate.gameObject.AddComponent<Image>();
img.sprite = UISkin.Icon(r.sprite);
img.color = r.color;
img.raycastTarget = false;
if (img.sprite != null && img.sprite.border.sqrMagnitude > 0f) img.type = Image.Type.Sliced;
for (int i = 0; i < name.Length; i++)
{
var t = UiKit.Title(UiKit.Label(box, name.Substring(i, 1), fs, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -(pad + i * ch)), new Vector2(w, ch)), true);
t.horizontalOverflow = HorizontalWrapMode.Overflow;
t.verticalOverflow = VerticalWrapMode.Overflow;
}
}
/// <summary>
/// 픽업 배너(PD 2026-11-13 참고 3 구성 = 왼쪽 정보 판 + 오른쪽 큰 일러스트 + 아래 이름표): 그림(영웅 일러 + 왼쪽 어둡게)은 한 장 ·
/// 왼쪽 = 종류 칩 → 제목(영웅 이름 강조) → ★ → 남은 기간 → 설명 → 인게임 그림 + 스킬 아이콘 → 천장 안내 · 오른쪽 아래 = 이름 명패 + 「상세 정보」.
/// </summary>
void DrawPickup()
{
var px = bannerPx;
var g = S.Db.Gacha;
var h = banner.featured.Count > 0 ? S.Def(banner.featured[0]) : null;
var now = S.Now;
bool open = GachaRules.IsOpen(banner, now);
var kind = UiKit.Skin(UiKit.At(px, "Kind", 74f, 24f, 176f, 52f), "chip", UiKit.Purple);
UiKit.Title(UiKit.FillLabel(kind.transform, "픽업 등용", 28, UiKit.White), true);
string title = banner.nameKo;
var names = new List<string>();
foreach (var id in banner.featured)
{
var fh = S.Def(id);
if (fh == null) continue;
names.Add(fh.nameKo);
title = title.Replace(fh.nameKo, "<color=#" + ColorUtility.ToHtmlStringRGB(UiKit.Star) + ">" + fh.nameKo + "</color>");
}
var tt = UiKit.Title(UiKit.Label(px, title, 68, UiKit.White, TextAnchor.MiddleLeft, TL, TL, new Vector2(74f, -82f), new Vector2(440f, 92f)), true);
tt.supportRichText = true;
tt.horizontalOverflow = HorizontalWrapMode.Overflow;
if (h != null)
for (int i = 0; i < h.rarity; i++)
UiKit.DropShadow(UiKit.Icon(px, "star_l", TL, TL, new Vector2(76f + i * 54f, -180f), 52f), 3f);
var clock = UiKit.Icon(px, "clock", TL, TL, new Vector2(76f, -246f), 40f);
string when = !open ? "등용 기간 종료" : GachaRules.EndOf(banner) == null ? "기간 제한 없음" : "남은 기간 " + RemainText(GachaRules.Remaining(banner, now));
var wt = UiKit.Title(UiKit.Label(px, when, 30, UiKit.White, TextAnchor.MiddleLeft, TL, TL, new Vector2(clock != null ? 126f : 76f, -245f), new Vector2(360f, 42f)), true);
wt.horizontalOverflow = HorizontalWrapMode.Overflow;
string desc = banner.descKo + "\n★★★ 등용 시 " + (g.PickupShare * 100f).ToString("0.#") + "% 확률로 " + string.Join(" · ", names.ToArray());
var dt = UiKit.Label(px, desc, 26, UiKit.White, TextAnchor.UpperLeft, TL, TL, new Vector2(76f, -302f), new Vector2(390f, 80f));
dt.lineSpacing = 1.15f;
UiKit.DropShadow(dt, 2f);
if (h != null)
{
// 인게임 그림 + 스킬 아이콘 2(액티브 · 패시브) — 누르면 상세 정보
const float y = 396f, cell = 108f;
var face = UiKit.Portrait("InGame", px, h, TL, TL, new Vector2(76f, -y), new Vector2(cell, cell), false, 1f, UiKit.Col("header", UiKit.Dark));
face.gameObject.AddComponent<Button>().onClick.AddListener(ShowBannerInfo);
Caption(px, "인게임", 76f, y + cell + 4f, cell);
string tk = UiKit.TypeKey(h.type);
for (int i = 0; i < 2; i++)
{
var sk = SkillRules.Skill(S.Db, h, i);
if (sk == null || UISkin.Icon("skill_" + sk.id) == null) continue;
float x = 76f + (i + 1) * (cell + 14f);
UiKit.Icon(px, "skill_" + sk.id, TL, TL, new Vector2(x + cell * 0.082f, -(y + cell * 0.082f)), cell * 0.836f);
if (tk != null) UiKit.DropShadow(UiKit.Icon(px, "skill_ring_" + tk, TL, TL, new Vector2(x, -y), cell), 3f);
Caption(px, i == 0 ? "액티브" : "패시브", x, y + cell + 4f, cell);
}
}
int left = Mathf.Max(1, g.Pity - GachaRules.Pity(S.Save, banner));
UiKit.InfoChip(px, "PityNote", 74f, 558f, 400f, 48f, null, "앞으로 " + left + "회 안에 ★★★ 확정", 26);
// 오른쪽 아래 = 이름 명패(병종 기호 · 이름 · ★) + 상세 정보
if (h != null)
{
var plate = UiKit.At(px, "Name", 500f, 530f, 348f, 82f);
var pimg = plate.gameObject.AddComponent<Image>();
pimg.raycastTarget = false;
pimg.sprite = UISkin.Icon("tre_plaque");
if (pimg.sprite != null)
{
if (pimg.sprite.border.sqrMagnitude > 0f) pimg.type = Image.Type.Sliced;
}
else
{
UISkin.Apply(pimg, "chip");
pimg.color = UiKit.Col("chip_dark", UiKit.Dark);
}
var row = UiKit.Fill("Row", plate, 62f, 62f, 6f, 12f);
var hl = row.gameObject.AddComponent<HorizontalLayoutGroup>();
hl.childAlignment = TextAnchor.MiddleCenter;
hl.spacing = 8f;
hl.childControlWidth = true;
hl.childControlHeight = false;
hl.childForceExpandWidth = hl.childForceExpandHeight = false;
var sym = UiKit.Icon(row, UiKit.TypeSymbolId(h.type), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 46f);
if (sym != null) Fixed(sym, 46f);
var nt = UiKit.Title(UiKit.Label(row, h.nameKo, 40, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(120f, 60f)), true);
nt.horizontalOverflow = HorizontalWrapMode.Overflow;
for (int i = 0; i < h.rarity; i++)
{
var st = UiKit.Icon(row, "star_on", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 30f);
if (st != null) Fixed(st, 26f);
}
UiKit.SkinBtn(px, "BannerInfo", "btn_sky", UiKit.White, "상세 정보", 28, UiKit.White, 856f, 530f, 150f, 80f, ShowBannerInfo);
}
}
static void Fixed(Graphic g, float w)
{
var le = g.gameObject.AddComponent<LayoutElement>();
le.minWidth = le.preferredWidth = w;
}
static void Caption(Transform px, string s, float x, float y, float w)
{
UiKit.DropShadow(UiKit.Label(px, s, 22, UiKit.Col("text_soft", UiKit.White), TextAnchor.MiddleCenter, TL, TL, new Vector2(x, -y), new Vector2(w, 30f), false), 2f);
}
/// <summary>남은 기간 글자(하루 이상 = n일 n시간 · 하루 미만 = n시간 n분).</summary>
public static string RemainText(TimeSpan r)
{
if (r.TotalDays >= 1.0) return (int)r.TotalDays + "일 " + r.Hours + "시간";
return (int)r.TotalHours + "시간 " + r.Minutes + "분";
}
/// <summary>같은 천장 묶음을 쓰는 배너 수.</summary>
int PityShare(GachaConfig.Banner b)
{
int n = 0;
foreach (var o in S.Db.Gacha.banners)
if (o.pityGroup == b.pityGroup) n++;
return n;
}
/// <summary>등용 규칙(구 각주 → ⓘ 팝업 · Fable §4-4) — 배너별 풀 · 천장 묶음 포함.</summary>
void ShowGachaRules()
{
var e = S.Db.Economy;
var g = S.Db.Gacha;
Root.Modal("뽑기 규칙",
"중복 = 영웅 조각\n★★★ " + e.ShardDup(3) + " · ★★ " + e.ShardDup(2) + " · ★ " + e.ShardDup(1) + "\n병사 중복 = 골드 " + e.SoldierDupGold +
"\n\n" + g.TenCount + "연 = ★★ 이상 1 보장\n천장 " + g.Pity + "회 = ★★★ 확정\n확률·천장 전부 공개",
string scope = "";
if (banner != null)
{
if (banner.IsPickup)
{
var names = new List<string>();
foreach (var id in banner.featured)
if (S.Def(id) != null) names.Add(S.Def(id).nameKo);
scope = "픽업 배너 = ★★★ 의 " + (g.PickupShare * 100f).ToString("0.#") + "% 가 " + string.Join(" · ", names.ToArray()) + "\n";
}
else scope = "상시 배너 = " + UiKit.FactionKo(banner.faction) + " 진영 영웅만 등장\n";
if (g.PickupExclusive) scope += "픽업 영웅은 픽업 배너에서만 등장\n";
int share = PityShare(banner);
scope += "천장 " + g.Pity + "회 = ★★★ 확정 (" + (share > 1 ? "배너 " + share + "개가 같이 셈" : "이 배너만 셈") + ")\n\n";
}
Root.Modal("등용 규칙",
scope + "중복 = 영웅 조각\n★★★ " + e.ShardDup(3) + " · ★★ " + e.ShardDup(2) + " · ★ " + e.ShardDup(1) + "\n병사 중복 = 골드 " + e.SoldierDupGold +
"\n\n" + g.TenCount + "회 등용 = ★★ 이상 1 보장\n확률·천장 전부 공개",
new OutGameRoot.ModalButton("확인", UiKit.Dark, null));
}
@ -197,22 +645,22 @@ namespace TKS.OutGame
S.EnsureShop();
}
// ---------- 뽑기 ----------
// ---------- 명장 등용(테마가 없을 때의 예전 화면) ----------
void BuildGacha(RectTransform p)
{
var g = S.Db.Gacha;
var banner = UiKit.TopBand("Banner", p, 12f, 440f, 24f, 24f);
if (UISkin.Has("banner")) UiKit.Skin(banner, "banner", Color.white); // 팩 배너 틀(PD #1100)
else UiKit.Paint(banner, new Color(0.27f, 0.14f, 0.12f));
var pick = UiKit.Box("Pickup", banner, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(30f, -24f), new Vector2(230f, 62f), UiKit.Red);
UiKit.FillLabel(pick.transform, "픽업 (2주)", 30, UiKit.White);
UiKit.Title(UiKit.Label(banner, "영웅·유닛 뽑기", 60, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(36f, -110f), new Vector2(600f, 90f)), true);
UiKit.Label(banner, "모든 영웅·병사 유닛\n= 뽑기로 획득", 32, new Color(0.9f, 0.82f, 0.78f), TextAnchor.UpperLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(36f, -210f), new Vector2(520f, 110f), false);
var free = UiKit.Box("Free", banner, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(36f, 28f), new Vector2(300f, 78f), new Color(0.93f, 0.75f, 0.24f));
UiKit.FillLabel(free.transform, "첫 10연 무료", 34, UiKit.Ink);
var bn = UiKit.TopBand("Banner", p, 12f, 440f, 24f, 24f);
if (UISkin.Has("banner")) UiKit.Skin(bn, "banner", Color.white); // 팩 배너 틀(PD #1100)
else UiKit.Paint(bn, new Color(0.27f, 0.14f, 0.12f));
var pick = UiKit.Box("Pickup", bn, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(30f, -24f), new Vector2(230f, 62f), UiKit.Red);
UiKit.FillLabel(pick.transform, "등용 배너", 30, UiKit.White);
UiKit.Title(UiKit.Label(bn, "명장 등용", 60, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(36f, -110f), new Vector2(600f, 90f)), true);
UiKit.Label(bn, "모든 영웅·병사 유닛\n= 등용으로 획득", 32, new Color(0.9f, 0.82f, 0.78f), TextAnchor.UpperLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(36f, -210f), new Vector2(520f, 110f), false);
var free = UiKit.Box("Free", bn, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(36f, 28f), new Vector2(300f, 78f), new Color(0.93f, 0.75f, 0.24f));
UiKit.FillLabel(free.transform, "첫 10회 무료", 34, UiKit.Ink);
freeChip = free.gameObject;
pickupArt = UiKit.Rect("PickupArt", banner, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-20f, 10f), new Vector2(440f, 400f));
pickupArt = UiKit.Rect("PickupArt", bn, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-20f, 10f), new Vector2(440f, 400f));
var pity = UiKit.TopBand("Pity", p, 468f, 130f, 24f, 24f);
var pimg = UiKit.Paint(pity, UiKit.Paper);
@ -239,67 +687,70 @@ namespace TKS.OutGame
singleBtn = UiKit.Btn("Single", btns, "", 40, UiKit.Dark, UiKit.White, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), Vector2.zero, new Vector2(500f, 160f), () => DoDraw(1));
tenBtn = UiKit.Btn("Ten", btns, "", 40, UiKit.Red, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(500f, 160f), () => DoDraw(g.TenCount));
var f2 = UiKit.Box("Free2", tenBtn.transform, new Vector2(1f, 1f), new Vector2(1f, 0.5f), new Vector2(-10f, 0f), new Vector2(240f, 56f), new Color(0.93f, 0.75f, 0.24f));
UiKit.FillLabel(f2.transform, "첫 10연 무료", 26, UiKit.Ink);
UiKit.FillLabel(f2.transform, "첫 10회 무료", 26, UiKit.Ink);
freeChip2 = f2.gameObject;
var e = S.Db.Economy;
UiKit.Label(p, "중복 = 영웅 조각(★★★ " + e.ShardDup(3) + " · ★★ " + e.ShardDup(2) + " · ★ " + e.ShardDup(1) + ") · 병사 중복 = 골드 " + e.SoldierDupGold +
" · 10연 = ★★ 이상 1 보장", 26, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -1090f), new Vector2(1020f, 80f), false);
" · 10회 등용 = ★★ 이상 1 보장", 26, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -1090f), new Vector2(1020f, 80f), false);
}
void RefreshGacha()
{
var g = S.Db.Gacha;
var d = S.Save;
if (banner == null) return;
bool free = GachaRules.FirstTenFree(S.Db, d);
freeChip.SetActive(free);
bool open = GachaRules.IsOpen(banner, S.Now);
int pity = GachaRules.Pity(d, banner);
if (freeChip != null) freeChip.SetActive(free);
freeChip2.SetActive(free);
singleBtn.interactable = tenBtn.interactable = open;
if (pityBadge != null)
{
pityText.text = d.gacha.pity + " / " + g.Pity + " · " + g.Pity + "회째 ★★★ 확정";
pityBadge.text = d.gacha.pity.ToString();
UiKit.SetGauge(pityBar, d.gacha.pity / (float)Mathf.Max(1, g.Pity), GaugeW + 60f);
pityText.text = pity + " / " + g.Pity + " · " + g.Pity + "회째 ★★★ 확정";
pityBadge.text = pity.ToString();
int share = PityShare(banner);
pityNote.text = share > 1 ? "배너 " + share + "개 공용" : "이 배너 전용";
UiKit.SetGauge(pityBar, pity / (float)Mathf.Max(1, g.Pity), GaugeW + 60f);
singleCost.text = g.CostSingle.ToString("N0");
tenCost.text = free ? "무료" : g.CostTen.ToString("N0");
var ic = tenCost.transform.parent.GetComponentInChildren<Image>(true);
if (ic != null) ic.gameObject.SetActive(!free);
}
else
{
pityText.text = d.gacha.pity + " / " + g.Pity + " · " + g.Pity + "회 ★★★ 확정";
UiKit.SetBar(pityBar, d.gacha.pity / (float)Mathf.Max(1, g.Pity));
UiKit.LabelOf(singleBtn).text = "1회\n<size=32>보옥 " + g.CostSingle.ToString("N0") + "</size>";
UiKit.LabelOf(tenBtn).text = g.TenCount + "연\n<size=32>" + (free ? "무료" : "보옥 " + g.CostTen.ToString("N0")) + "</size>";
}
UiKit.Clear(pickupArt);
var picks = new List<HeroDb.Hero>();
foreach (var pe in g.pool)
if (pe.pickup && pe.inPool && S.Def(pe.heroId) != null) picks.Add(S.Def(pe.heroId));
if (pityBadge != null)
{
var arts = picks.FindAll(h => UiKit.ArtSprite(h.art) != null);
for (int i = 0; i < arts.Count && i < 2; i++)
UiKit.Portrait("Pick" + i, pickupArt, arts[i], new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-10f - i * 250f, 70f), new Vector2(300f, 440f), false, 1f, new Color(1f, 1f, 1f, 0f));
for (int i = 0; i < picks.Count && i < 2; i++)
UiKit.InfoChip(pickupArt, "Name" + i, 560f - (i + 1) * 250f, 498f, 230f, 50f, null, picks[i].nameKo + " ★" + picks[i].rarity, 26);
// 확률 행 메모 = 배너별(★★★ = 픽업 영웅 몫 · ★ = 병사 포함)
var names = new List<string>();
if (banner.IsPickup)
foreach (var id in banner.featured)
if (S.Def(id) != null) names.Add(S.Def(id).nameKo);
probNote[3].text = names.Count > 0 ? string.Join("·", names.ToArray()) + " " + (g.PickupShare * 100f).ToString("0.#") + "%" : "";
probNote[2].text = "";
probNote[1].text = banner.soldiers ? "병사 포함" : "";
DrawBanner();
return;
}
for (int i = 0; i < picks.Count && i < 2; i++)
pityText.text = pity + " / " + g.Pity + " · " + g.Pity + "회 ★★★ 확정";
UiKit.SetBar(pityBar, pity / (float)Mathf.Max(1, g.Pity));
UiKit.LabelOf(singleBtn).text = "1회 등용\n<size=32>보옥 " + g.CostSingle.ToString("N0") + "</size>";
UiKit.LabelOf(tenBtn).text = g.TenCount + "회 등용\n<size=32>" + (free ? "무료" : "보옥 " + g.CostTen.ToString("N0")) + "</size>";
UiKit.Clear(pickupArt);
for (int i = 0; i < banner.featured.Count && i < 2; i++)
{
UiKit.Portrait("Pick" + i, pickupArt, picks[i], new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-i * 210f, 50f), new Vector2(230f, 330f), false, 1f, new Color(1f, 1f, 1f, 0f));
UiKit.Label(pickupArt, picks[i].nameKo + " ★" + picks[i].rarity, 26, UiKit.White, TextAnchor.MiddleCenter, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-i * 210f, 6f), new Vector2(230f, 44f));
var h = S.Def(banner.featured[i]);
if (h == null) continue;
UiKit.Portrait("Pick" + i, pickupArt, h, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-i * 210f, 50f), new Vector2(230f, 330f), false, 1f, new Color(1f, 1f, 1f, 0f));
UiKit.Label(pickupArt, h.nameKo + " ★" + h.rarity, 26, UiKit.White, TextAnchor.MiddleCenter, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-i * 210f, 6f), new Vector2(230f, 44f));
}
}
public void DoDraw(int count)
{
var why = GachaRules.CanDraw(S.Db, S.Save, count);
var why = GachaRules.CanDraw(S.Db, S.Save, banner, count, S.Now);
if (why != null)
{
Root.Toast(why);
return;
}
LastPulls = S.Draw(count);
LastPulls = S.Draw(banner, count);
ShowResult(LastPulls);
}
@ -323,7 +774,7 @@ namespace TKS.OutGame
skip.transition = Selectable.Transition.None;
skip.onClick.AddListener(RevealAll);
resultRoot = dim.gameObject;
UiKit.Label(dim, "뽑기 결과", 64, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -200f), new Vector2(900f, 100f));
UiKit.Label(dim, "등용 결과", 64, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -200f), new Vector2(900f, 100f));
int cols = Mathf.Min(5, Mathf.Max(1, pulls.Count));
int rows = (pulls.Count + cols - 1) / cols;
float cw = 196f, chh = 330f;
@ -390,7 +841,7 @@ namespace TKS.OutGame
if (resultRoot != null)
{
resultRoot.SetActive(false);
Object.Destroy(resultRoot);
UnityEngine.Object.Destroy(resultRoot);
resultRoot = null;
}
Refresh();

View File

@ -124,6 +124,7 @@ namespace TKS.OutGame
root.Show(root.Trade);
root.Trade.SetPage(0);
yield return Shot(cam, "trade_gacha", W, H);
yield return RecruitShots(cam, root); // 명장 등용 배너(PD 2026-11-13 · 픽업 · 국가별 상시 4 · 상세 정보 팝업)
yield return GachaShots(cam, root); // 뽑기 연출(삼고초려 장면 → 카드 획득 · PD 2026-11-13 · -uiOnly gacha_cut1|2|3 · gacha_cards 일 때만)
root.Show(root.Treasures);
root.Treasures.SetPage(2);
@ -414,9 +415,66 @@ namespace TKS.OutGame
/// <summary>캡처용 뽑기 1장 = 등급 tier 풀의 k 번째 유닛 · 세이브에도 실제 뽑기처럼(새 유닛 = 획득 · 중복 = 조각 / 병사 = 골드 · GachaRules.Draw 와 같은 처리).</summary>
const string GachaShowcase = "zhugeliang";
/// <summary>
/// 명장 등용 배너 캡처(PD 2026-11-13): trade_recruit_pickup(픽업 배너) · trade_recruit_&lt;배너 id&gt;(국가별 상시 = 진영 깃발 고름) ·
/// trade_recruit_info(픽업 영웅 상세 정보 팝업) · trade_recruit_info_hero(상시 배너 가운데 영웅 상세 정보). 기간이 지난 뒤에도 찍히게 시계를 픽업 기간 안으로 둔다(캡처 동안만).
/// </summary>
IEnumerator RecruitShots(Camera cam, OutGameRoot root)
{
var s = root.Session;
var clock = s.Clock;
GachaConfig.Banner pick = null;
foreach (var b in s.Db.Gacha.banners)
if (b.IsPickup && pick == null) pick = b;
if (pick != null && !GachaRules.IsOpen(pick, s.Now) && GachaRules.StartOf(pick) != null)
{
var inside = GachaRules.StartOf(pick).Value.AddHours(22.5);
s.Clock = () => inside;
}
if (pick != null)
{
root.Trade.SelectBanner(pick.id);
yield return Shot(cam, "trade_recruit_pickup", W, H);
yield return Shot(cam, "trade_recruit_pickup_1080x2400", 1080, 2400); // 좁고 긴 화면(9:20)
yield return Shot(cam, "trade_recruit_pickup_1536x2048", W2, H2); // 넓은 화면(3:4)
Log("recruit pickup banner=" + root.Trade.Banner.id + " page=" + root.Trade.BannerPage + "/" + root.Trade.BannerPageCount + " open=" + GachaRules.IsOpen(pick, s.Now));
}
GachaConfig.Banner first = null;
foreach (var b in s.Db.Gacha.banners)
{
if (b.IsPickup) continue;
if (first == null) first = b;
root.Trade.SelectBanner(b.id);
yield return Shot(cam, "trade_recruit_" + b.id, W, H);
Log("recruit " + b.id + " featured=" + root.Trade.FeaturedShown + " flags=" + root.Trade.FlagCount);
}
if (pick != null)
{
root.Trade.SelectBanner(pick.id);
root.Trade.ShowBannerInfo();
yield return Shot(cam, "trade_recruit_info", W, H);
var st = HeroInfoPopup.LastStats;
Log("recruit info " + HeroInfoPopup.LastHeroId + " hp=" + st.Hp + " atk=" + st.Atk + " def=" + st.Def + " power=" + st.power);
root.CloseModal();
}
if (first != null && first.featured.Count > 0)
{
root.Trade.SelectBanner(first.id);
root.Trade.ShowHeroInfo(first.featured[0]);
yield return Shot(cam, "trade_recruit_info_hero", W, H);
root.CloseModal();
}
s.Clock = clock;
root.Trade.SelectBanner(GachaRules.Default(s.Db, s.Now).id);
}
static GachaPull CapturePull(OutGameSession s, int tier, int k)
{
var list = GachaRules.Candidates(s.Db, tier);
// 전 진영이 나오는 배너의 풀(없으면 지금 열린 첫 배너)
GachaConfig.Banner all = null;
foreach (var b in s.Db.Gacha.banners)
if (all == null && b.pool.Contains(GachaConfig.PoolAll)) all = b;
var list = GachaRules.Candidates(s.Db, all ?? GachaRules.Default(s.Db, s.Now), tier);
var h = s.Def(list[k % list.Count].heroId);
// ★★★ 첫 장 = 제갈량(삼고초려 장면 확인용 · 풀에 있을 때만 · 캡처 전용 선택 — 게임 확률과 무관)
if (tier == 3 && k == 0)

View File

@ -42,7 +42,7 @@ namespace TKS.Tests
GachaPull Pull(int tier)
{
var s = og.Session;
var list = GachaRules.Candidates(s.Db, tier);
var list = GachaRules.Candidates(s.Db, og.Trade.Banner, tier);
Assert.Greater(list.Count, 0, "등급 " + tier + " 풀");
var h = s.Def(list[0].heroId);
var p = new GachaPull { heroId = h.id, tier = tier };

View File

@ -290,27 +290,33 @@ namespace TKS.Tests
{
var g = Db.Gacha;
Assert.AreEqual(1f, g.P(1) + g.P(2) + g.P(3), 1e-4f, "확률 합 = 1");
// 전 진영이 나오는 배너(등급 풀 = 영웅 + 병사 전부 · 배너별 규칙은 RecruitTests) · 시각 = 그 배너 기간 안
GachaConfig.Banner b = null;
foreach (var x in g.banners)
if (b == null && x.pool.Contains(GachaConfig.PoolAll)) b = x;
Assert.IsNotNull(b, "pool = all 배너");
var at = GachaRules.StartOf(b) ?? T0;
var rng = new System.Random(1093);
var d = NewSave();
int gem = d.gem;
Assert.IsTrue(GachaRules.FirstTenFree(Db, d));
var ten = GachaRules.Draw(Db, d, g.TenCount, rng);
var ten = GachaRules.Draw(Db, d, b, g.TenCount, rng, at);
Assert.AreEqual(g.TenCount, ten.Count);
Assert.AreEqual(gem, d.gem, "첫 10연 무료");
Assert.IsTrue(d.gacha.firstTenUsed);
Assert.AreEqual(g.CostTen, GachaRules.Cost(Db, d, g.TenCount));
d.gem = 100000000;
d.gacha.pity = g.Pity - 1;
var p = GachaRules.Draw(Db, d, 1, rng);
d.gacha.SetPity(b.pityGroup, g.Pity - 1);
var p = GachaRules.Draw(Db, d, b, 1, rng, at);
Assert.AreEqual(3, p[0].tier, "천장 = ★★★ 확정");
Assert.IsTrue(p[0].pity);
Assert.AreEqual(0, d.gacha.pity, "획득 시 리셋");
Assert.AreEqual(0, GachaRules.Pity(d, b), "획득 시 리셋");
Assert.AreEqual(3, Db.Heroes.Get(p[0].heroId).rarity);
for (int k = 0; k < 200; k++)
{
var batch = GachaRules.Draw(Db, d, g.TenCount, rng);
var batch = GachaRules.Draw(Db, d, b, g.TenCount, rng, at);
bool ok = false;
foreach (var x in batch) ok |= x.tier >= g.TenGuaranteeRarity;
Assert.IsTrue(ok, "10연 = ★★ 이상 1 보장");
@ -320,8 +326,8 @@ namespace TKS.Tests
int n = 20000, shards = 0, gold = 0;
for (int k = 0; k < n; k++)
{
d.gacha.pity = 0;
var x = GachaRules.Draw(Db, d, 1, rng)[0];
d.gacha.SetPity(b.pityGroup, 0);
var x = GachaRules.Draw(Db, d, b, 1, rng, at)[0];
counts[x.tier]++;
shards += x.shards;
gold += x.gold;
@ -401,7 +407,7 @@ namespace TKS.Tests
d.inv.AddGear(Db.Gear.items[0].id, 3);
d.Stage(1, 2, true).stars = 2;
d.presets[1].slots[0] = "";
d.gacha.pity = 7;
d.gacha.SetPity(Db.Gacha.banners[0].pityGroup, 7);
SaveStore.Write(d, T0);
var l = SaveStore.Load();
Assert.IsNotNull(l);

View File

@ -0,0 +1,385 @@
using System;
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using TKS.OutGame;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using UnityEngine.UI;
namespace TKS.Tests
{
/// <summary>
/// 명장 등용(뽑기 메뉴 개편 · PD 2026-11-13) — ① 배너 표(gacha_banners.csv): 픽업 + 진영마다 상시 배너 · 주요 영웅이 그 배너 풀에서 나옴 · 배치 표(recruit_banner.csv)에 자리·깃발이 있음
/// ② 상시 배너 = 그 진영 영웅(+ 병사)만 · 픽업 영웅은 상시 배너에서 안 나옴(pickup_exclusive) ③ 픽업 배너 = ★★★ 의 pickup_share 가 주요 영웅
/// ④ 천장 = pity_group 끼리 같이 셈 · 다른 묶음은 따로 ⑤ 기간(start + days) 밖 = 등용 불가(보옥 그대로) ⑥ 구 세이브 천장 → 상시 묶음으로 옮김
/// ⑦ 풀 성장 = 최고 레벨·등급·★·장비 전부(표 값) ⑧ 화면: 탭 이름 「명장 등용」 · 「뽑기」 글자 없음 · 좌우 쪽 넘김 · 깃발 = 배너 고르기 · 주요 영웅 5 · 상세 정보 팝업 · 고른 배너 풀로 등용.
/// 기대 값은 전부 표에서 읽는다(C45).
/// </summary>
public class RecruitTests
{
static readonly DateTime T0 = new DateTime(2026, 11, 5, 3, 0, 0, DateTimeKind.Utc);
OutGameRoot og;
static OutGameData Db
{
get { return OutGameData.Default; }
}
static BalanceConfig Cfg
{
get { return Resources.Load<BalanceConfig>("BalanceConfig"); }
}
static SaveData RichSave()
{
var d = HeroRules.NewSave(Db, Cfg.playerSpawnPoints.Length, T0);
d.gem = 100000000;
d.gacha.firstTenUsed = true;
return d;
}
/// <summary>그 배너가 열려 있는 시각(기간 있음 = 시작 + 1시간 · 없음 = T0).</summary>
static DateTime Inside(GachaConfig.Banner b)
{
var s = GachaRules.StartOf(b);
return s != null ? s.Value.AddHours(1) : T0;
}
static List<GachaConfig.Banner> Banners(bool pickup)
{
var l = new List<GachaConfig.Banner>();
foreach (var b in Db.Gacha.banners)
if (b.IsPickup == pickup) l.Add(b);
return l;
}
[TearDown]
public void TearDown()
{
GachaCutscene.Manual = false;
SaveStore.UseMemory(false);
}
// ---------- 표 ----------
[Test]
public void Banners_Table_PickupAndFactionStanding_FeaturedInPool_LayoutRows()
{
var g = Db.Gacha;
Assert.Greater(Banners(true).Count, 0, "픽업 배너");
// 진영마다 상시 배너 1(영웅이 있는 진영 전부)
var factions = new HashSet<string>();
foreach (var h in Db.Heroes.heroes)
if (!h.isSoldier) factions.Add(h.faction);
foreach (var f in factions)
{
int n = 0;
foreach (var b in Banners(false))
if (b.faction == f && b.pool.Contains(f)) n++;
Assert.AreEqual(1, n, "진영 " + f + " 상시 배너");
Assert.IsNotNull(RecruitBanner.First("flag_" + f), "배치 표 깃발 flag_" + f);
Assert.IsNotNull(UISkin.Icon(RecruitBanner.First("flag_" + f).sprite), "깃발 그림 " + f);
}
Assert.IsNotNull(RecruitBanner.First("map"), "배치 표 지도");
Assert.IsNotNull(UISkin.Icon(RecruitBanner.First("map").sprite), "지도 그림");
foreach (var b in g.banners)
{
Assert.IsNotNull(UISkin.Icon(b.art), b.id + " 배너 그림 " + b.art);
Assert.Greater(b.featured.Count, 0, b.id);
foreach (var id in b.featured)
{
var h = Db.Heroes.Get(id);
Assert.IsNotNull(h, b.id + " featured " + id);
Assert.IsTrue(GachaRules.InBanner(g, Db.Heroes, b, h), b.id + " 주요 영웅 " + id + " 가 풀에서 나옴");
Assert.IsNotNull(UiKit.ArtSprite(h.art), id + " 인게임 그림");
}
if (b.IsPickup) continue;
Assert.AreEqual(b.featured.Count, RecruitBanner.Slots("hero", b.featured.Count).Count, b.id + " 영웅 자리 수");
Assert.AreEqual(b.featured.Count, RecruitBanner.Slots("tag", b.featured.Count).Count, b.id + " 이름표 수");
}
}
// ---------- 규칙 ----------
[Test]
public void Standing_OnlyThatFaction_PickupHeroExcluded()
{
var g = Db.Gacha;
var rng = new System.Random(1113);
foreach (var b in Banners(false))
{
var d = RichSave();
var seen = new HashSet<string>();
for (int k = 0; k < 300; k++)
foreach (var p in GachaRules.Draw(Db, d, b, g.TenCount, rng, Inside(b)))
{
var h = Db.Heroes.Get(p.heroId);
seen.Add(h.id);
if (h.isSoldier) Assert.IsTrue(b.soldiers, b.id + " 병사 " + h.id);
else Assert.IsTrue(b.pool.Contains(GachaConfig.PoolAll) || b.pool.Contains(h.faction), b.id + " 에서 다른 진영 " + h.id + "(" + h.faction + ")");
if (g.PickupExclusive) Assert.IsFalse(GachaRules.IsPickupHero(g, h.id), b.id + " 에서 픽업 영웅 " + h.id);
Assert.IsFalse(p.pickup, "상시 배너 = 픽업 표시 없음");
}
foreach (var id in b.featured) Assert.IsTrue(seen.Contains(id), b.id + " 주요 영웅 " + id + " 가 3000회 안에 나옴");
}
}
[Test]
public void Pickup_FeaturedShare_OfTopTier()
{
var g = Db.Gacha;
var rng = new System.Random(1114);
foreach (var b in Banners(true))
{
var d = RichSave();
int n = 4000, hit = 0;
for (int k = 0; k < n; k++)
{
d.gacha.SetPity(b.pityGroup, g.Pity - 1); // 천장 = ★★★ 확정
var p = GachaRules.Draw(Db, d, b, 1, rng, Inside(b))[0];
Assert.AreEqual(3, p.tier);
Assert.AreEqual(b.featured.Contains(p.heroId), p.pickup, p.heroId + " 픽업 표시");
if (p.pickup) hit++;
}
Assert.AreEqual(g.PickupShare, hit / (float)n, 0.03f, b.id + " ★★★ 중 주요 영웅 비율 ≈ pickup_share");
}
}
[Test]
public void Pity_SharedInsideGroup_SeparateAcrossGroups()
{
var g = Db.Gacha;
var rng = new System.Random(1115);
foreach (var a in g.banners)
{
var d = RichSave();
var before = new Dictionary<string, int>();
foreach (var o in g.banners) before[o.id] = GachaRules.Pity(d, o);
var pulls = GachaRules.Draw(Db, d, a, 1, rng, Inside(a));
Assert.AreEqual(1, pulls.Count);
int expect = pulls[0].tier == 3 ? 0 : 1;
foreach (var o in g.banners)
{
if (o.pityGroup == a.pityGroup) Assert.AreEqual(expect, GachaRules.Pity(d, o), a.id + " → " + o.id + " 같은 묶음 = 같이 셈");
else Assert.AreEqual(before[o.id], GachaRules.Pity(d, o), a.id + " → " + o.id + " 다른 묶음 = 그대로");
}
}
}
[Test]
public void Period_OutsideIsClosed_NoGemSpent()
{
var g = Db.Gacha;
var rng = new System.Random(1116);
foreach (var b in g.banners)
{
var start = GachaRules.StartOf(b);
var end = GachaRules.EndOf(b);
if (start == null)
{
Assert.IsTrue(GachaRules.IsOpen(b, T0) && GachaRules.IsOpen(b, T0.AddYears(5)), b.id + " 기간 없음 = 항상 열림");
continue;
}
Assert.IsTrue(GachaRules.IsOpen(b, start.Value), b.id + " 시작 시각 = 열림");
Assert.IsFalse(GachaRules.IsOpen(b, start.Value.AddSeconds(-1)), b.id + " 시작 전");
if (end == null) continue;
Assert.AreEqual(b.days, (end.Value - start.Value).TotalDays, 1e-6, b.id + " 기간 = days");
Assert.IsTrue(GachaRules.IsOpen(b, end.Value.AddSeconds(-1)), b.id + " 끝 직전");
Assert.IsFalse(GachaRules.IsOpen(b, end.Value), b.id + " 끝 시각 = 닫힘");
Assert.AreEqual(TimeSpan.Zero, GachaRules.Remaining(b, end.Value));
var d = RichSave();
int gem = d.gem;
Assert.IsNotNull(GachaRules.CanDraw(Db, d, b, 1, end.Value), b.id + " 기간 뒤 = 불가 사유");
Assert.AreEqual(0, GachaRules.Draw(Db, d, b, 1, rng, end.Value).Count);
Assert.AreEqual(gem, d.gem, "닫힌 배너 = 보옥 그대로");
}
}
[Test]
public void Sanitize_LegacyPity_MovesToStandingGroup()
{
var standing = Banners(false);
if (standing.Count == 0) Assert.Ignore("상시 배너 없음");
var d = HeroRules.NewSave(Db, Cfg.playerSpawnPoints.Length, T0);
d.gacha.pity = 5; // 구 세이브(배너 하나 시절)
HeroRules.Sanitize(Db, d, Cfg.playerSpawnPoints.Length);
Assert.AreEqual(0, d.gacha.pity);
Assert.AreEqual(5, d.gacha.PityOf(standing[0].pityGroup), "구 천장 → 상시 묶음");
}
[Test]
public void FullGrowth_IsTableMaximum()
{
foreach (var b in Db.Gacha.banners)
foreach (var id in b.featured)
{
var h = Db.Heroes.Get(id);
var full = HeroRules.FullGrowth(Db, h);
Assert.AreEqual(Db.Levels.MaxLevel, full.lv, id);
Assert.AreEqual(Db.Evolve.MaxStar, full.star, id);
Assert.AreEqual("", Db.Promotion.Get(full.grade).nextGrade, id + " 최고 등급 = 다음 등급 없음");
int gear = 0;
foreach (var set in h.gear) gear += set.slots.Count;
Assert.AreEqual(gear, full.gearTotal, id + " 승급 장비 전부");
for (int i = 0; i < 2; i++) Assert.AreEqual(SkillRules.Cap(Db, h, full, i), SkillRules.Level(full, i), id + " 스킬 " + i + " = 상한");
var top = HeroRules.Compute(Db, Cfg, h, full);
var first = HeroRules.Compute(Db, Cfg, h, HeroRules.NewHero(Db, h, 0));
Assert.Greater(top.Hp, first.Hp, id);
Assert.Greater(top.Atk, first.Atk, id);
Assert.Greater(top.power, first.power, id);
}
}
// ---------- 화면 ----------
IEnumerator Boot()
{
WorldProgress.Reset();
SaveStore.UseMemory(true);
SceneManager.LoadScene("Battle");
yield return null;
yield return null;
og = OutGameRoot.Instance;
Assert.IsNotNull(og);
}
static List<string> Texts(Transform root)
{
var l = new List<string>();
foreach (var t in root.GetComponentsInChildren<Text>(true)) l.Add(t.text ?? "");
return l;
}
[UnityTest]
public IEnumerator Screen_Names_Pages_Flags_Featured_Info_Draw()
{
yield return Boot();
var s = og.Session;
var g = s.Db.Gacha;
var picks = Banners(true);
var standing = Banners(false);
if (picks.Count > 0)
{
var inside = Inside(picks[0]);
s.Clock = () => inside; // 픽업 기간 안(날짜가 지나도 같은 화면)
}
og.OnTab(4);
og.Trade.SetPage(0);
og.Trade.SelectBanner(GachaRules.Default(s.Db, s.Now).id);
yield return null;
// 이름 = 「명장 등용」 · 「등용」 버튼 · 「뽑기」 글자 없음
var texts = Texts(og.Trade.Rt);
Assert.IsTrue(texts.Contains("명장 등용"), "거래 탭 이름");
Assert.IsTrue(texts.Contains("1회 등용"), "1회 등용 버튼");
Assert.IsTrue(texts.Contains(g.TenCount + "회 등용"), "10회 등용 버튼");
foreach (var t in texts) Assert.IsFalse(t.Contains("뽑기"), "「뽑기」 글자 남음: " + t);
StringAssert.Contains("명장 등용", CollectionCards.InfoText(s.Db.Heroes.Get(g.banners[0].featured[0]), false));
// 쪽 = 픽업 배너 수 + 상시 묶음 1 · 화살표 = 넘김(끝에서 돈다)
Assert.AreEqual(picks.Count + (standing.Count > 0 ? 1 : 0), og.Trade.BannerPageCount);
Assert.AreEqual(GachaRules.Default(s.Db, s.Now).id, og.Trade.Banner.id, "처음 = 표 순서의 첫 열린 배너");
int page0 = og.Trade.BannerPage;
for (int i = 0; i < og.Trade.BannerPageCount; i++) og.Trade.StepBanner(1);
Assert.AreEqual(page0, og.Trade.BannerPage, "한 바퀴 = 제자리");
// 상시 = 깃발마다 그 진영 배너 · 주요 영웅 전부 섬 · 깃발 수 = 상시 배너 수
foreach (var b in standing)
{
og.Trade.SelectBanner(b.id);
yield return null;
Assert.AreEqual(b.id, og.Trade.Banner.id);
Assert.AreEqual(b.featured.Count, og.Trade.FeaturedShown, b.id + " 주요 영웅");
Assert.AreEqual(standing.Count, og.Trade.FlagCount, b.id + " 깃발");
var names = Texts(og.Trade.Rt);
Assert.IsTrue(names.Contains(b.nameKo), b.id + " 제목");
}
if (standing.Count > 1 && picks.Count > 0)
{
// 쪽을 넘겼다 돌아오면 고른 진영 그대로
string last = standing[standing.Count - 1].id;
og.Trade.SelectBanner(last);
og.Trade.SelectBanner(picks[0].id);
Assert.IsTrue(og.Trade.Banner.IsPickup);
for (int i = 0; i < og.Trade.BannerPageCount && og.Trade.Banner.IsPickup; i++) og.Trade.StepBanner(1);
Assert.AreEqual(last, og.Trade.Banner.id, "상시 쪽 = 마지막에 고른 진영");
}
// 깃발 버튼을 누르면 그 배너(실제 Button)
if (standing.Count > 1)
{
og.Trade.SelectBanner(standing[0].id);
yield return null;
Button other = null;
foreach (var btn in og.Trade.Rt.GetComponentsInChildren<Button>(false))
if (btn.name == "Flag_" + standing[1].id) other = btn;
Assert.IsNotNull(other, "깃발 버튼 Flag_" + standing[1].id);
other.onClick.Invoke();
Assert.AreEqual(standing[1].id, og.Trade.Banner.id);
}
// 픽업 = 상세 정보 팝업(풀 성장 수치 = 표 계산값)
if (picks.Count > 0)
{
var b = picks[0];
og.Trade.SelectBanner(b.id);
yield return null;
Button info = null;
foreach (var btn in og.Trade.Rt.GetComponentsInChildren<Button>(false))
if (btn.name == "BannerInfo") info = btn;
Assert.IsNotNull(info, "상세 정보 버튼");
info.onClick.Invoke();
Assert.IsTrue(og.ModalVisible, "상세 정보 팝업");
Assert.AreEqual(b.featured[0], HeroInfoPopup.LastHeroId);
var h = s.Def(b.featured[0]);
var want = HeroRules.Compute(s.Db, s.Cfg, h, HeroRules.FullGrowth(s.Db, h));
Assert.AreEqual(want.power, HeroInfoPopup.LastStats.power);
Assert.AreEqual(want.Hp, HeroInfoPopup.LastStats.Hp);
og.CloseModal();
}
// 등용 = 고른 배너의 풀(상시 = 그 진영) · 연출로 이어짐
if (standing.Count > 0)
{
var b = standing[standing.Count - 1];
og.Trade.SelectBanner(b.id);
s.Save.gem = GachaRules.Cost(s.Db, s.Save, 1) + 1;
GachaCutscene.Manual = true;
og.Trade.DoDraw(1);
Assert.IsNotNull(og.Trade.LastPulls);
Assert.AreEqual(1, og.Trade.LastPulls.Count);
var got = s.Def(og.Trade.LastPulls[0].heroId);
Assert.IsTrue(got.isSoldier || b.pool.Contains(got.faction), b.id + " 에서 " + got.id);
Assert.IsTrue(og.Trade.ResultVisible, "등용 결과(연출)");
og.Trade.RevealAll();
og.Trade.CloseResult();
yield return null;
}
}
[UnityTest]
public IEnumerator Screen_ClosedPickup_ButtonsOff_Toast()
{
yield return Boot();
var s = og.Session;
GachaConfig.Banner b = null;
foreach (var x in Banners(true))
if (b == null && GachaRules.EndOf(x) != null) b = x;
if (b == null) Assert.Ignore("기간 있는 픽업 배너 없음");
var after = GachaRules.EndOf(b).Value.AddHours(1);
s.Clock = () => after;
og.OnTab(4);
og.Trade.SetPage(0);
og.Trade.SelectBanner(b.id);
yield return null;
Assert.IsTrue(Texts(og.Trade.Rt).Contains("등용 기간 종료"));
int gem = s.Save.gem;
og.Trade.DoDraw(1);
Assert.IsFalse(og.Trade.ResultVisible);
Assert.AreEqual(gem, s.Save.gem);
Assert.IsNotEmpty(og.LastToast);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c09e82730962fb5419e7a354c7329c72

View File

@ -190,10 +190,6 @@ SpriteAtlasAsset:
- {fileID: 2800000, guid: 32f85565e8b7a1344aabba239537fc17, type: 3}
- {fileID: 2800000, guid: 61c36c09211a0b34ca06fdbadaf1f964, type: 3}
- {fileID: 2800000, guid: 3bb26b9e5fda01a4f82f328bc0093baa, type: 3}
- {fileID: 2800000, guid: eb396bdf579952f49be3d6144ebf7a16, type: 3}
- {fileID: 2800000, guid: 13f7d098c72b722409320e5e6631bfe2, type: 3}
- {fileID: 2800000, guid: 7ea66fd4d34772748875511032d773e3, type: 3}
- {fileID: 2800000, guid: 2ff8381ec78b5ae4b9f9158111fe6fdc, type: 3}
- {fileID: 2800000, guid: 7703f9c4343c53843b43fca9b982bfaf, type: 3}
- {fileID: 2800000, guid: aea4d32da9d6a6b43b631243c524840c, type: 3}
- {fileID: 2800000, guid: def91387a43563446bec42d3bb438eec, type: 3}

View File

@ -38,6 +38,6 @@ SpriteAtlasImporter:
secondaryTextureSettings: {}
variantMultiplier: 1
bindAsDefault: 1
userData: 22ab0136a2fbbc1bacc47eea76205049
userData: df4c304bcd8447e6d5ed08270766d9b8
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,20 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!612988286 &1
SpriteAtlasAsset:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
serializedVersion: 2
m_MasterAtlas: {fileID: 0}
m_ImporterData:
packables:
- {fileID: 2800000, guid: eb396bdf579952f49be3d6144ebf7a16, type: 3}
- {fileID: 2800000, guid: 13f7d098c72b722409320e5e6631bfe2, type: 3}
- {fileID: 2800000, guid: 7ea66fd4d34772748875511032d773e3, type: 3}
- {fileID: 2800000, guid: 2ff8381ec78b5ae4b9f9158111fe6fdc, type: 3}
- {fileID: 2800000, guid: 277f35bc5cb3e6d48b867fc870307944, type: 3}
m_IsVariant: 0
m_ScriptablePacker: {fileID: 0}

View File

@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 0168b607c00ba3f40a691a4c8b43f88e
SpriteAtlasImporter:
externalObjects: {}
textureSettings:
serializedVersion: 2
anisoLevel: 0
compressionQuality: 0
maxTextureSize: 0
textureCompression: 0
filterMode: 2
generateMipMaps: 1
readable: 0
crunchedCompression: 0
sRGB: 1
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 1024
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
packingSettings:
serializedVersion: 2
padding: 4
blockOffset: 1
allowAlphaSplitting: 0
enableRotation: 0
enableTightPacking: 0
enableAlphaDilation: 1
secondaryTextureSettings: {}
variantMultiplier: 1
bindAsDefault: 1
userData: 91492ed3673be3d4558551c77e064221
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fa1e1baa32ffaa546907e420b585b9d3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

View File

@ -0,0 +1,168 @@
fileFormatVersion: 2
guid: b2b554a86e7fbd44092b455ec9603057
TextureImporter:
internalIDToNameTable:
- first:
213: 8473151847371029095
second: recruit_bg_other_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 1024
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: recruit_bg_other_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 1016
height: 624
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 76abe7666bea69570800000000000000
internalID: 8473151847371029095
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

View File

@ -0,0 +1,168 @@
fileFormatVersion: 2
guid: 7be45317ea022cb4eb13eb935a6f4975
TextureImporter:
internalIDToNameTable:
- first:
213: 5707632669473362343
second: recruit_bg_shu_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 1024
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: recruit_bg_shu_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 1016
height: 624
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 7a1df5c2ca5953f40800000000000000
internalID: 5707632669473362343
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

View File

@ -0,0 +1,168 @@
fileFormatVersion: 2
guid: f0f9add628d14e74eb70edfdb0089368
TextureImporter:
internalIDToNameTable:
- first:
213: -4364464521769003903
second: recruit_bg_wei_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 1024
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: recruit_bg_wei_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 1016
height: 624
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 184d92134ae4e63c0800000000000000
internalID: -4364464521769003903
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 KiB

View File

@ -0,0 +1,168 @@
fileFormatVersion: 2
guid: 0bfef021aaab1244d87e8b5666d3d9ae
TextureImporter:
internalIDToNameTable:
- first:
213: -6950898521693107379
second: recruit_bg_wu_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 1024
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: recruit_bg_wu_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 1016
height: 624
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: d4bc1b83f92798f90800000000000000
internalID: -6950898521693107379
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

View File

@ -0,0 +1,168 @@
fileFormatVersion: 2
guid: 277f35bc5cb3e6d48b867fc870307944
TextureImporter:
internalIDToNameTable:
- first:
213: -5351037008746549838
second: recruit_map_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 1024
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: recruit_map_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 380
height: 384
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 2bd61b9013c4db5b0800000000000000
internalID: -5351037008746549838
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

View File

@ -0,0 +1,168 @@
fileFormatVersion: 2
guid: 5279d758233ccdd488a3e2df62689fc9
TextureImporter:
internalIDToNameTable:
- first:
213: 9126107431752631624
second: recruit_pickup_lvbu_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 1024
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: recruit_pickup_lvbu_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 1016
height: 624
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 8450ba36b5276ae70800000000000000
internalID: 9126107431752631624
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant: