보물 · 연무장 · 내정(순찰) · 전투 HUD 비주얼 개선 = Fable 제안서 §4-5~§4-8 구현(#1104 · PD 2026-11-09)

- 보물: 목록 바탕 · 서브탭 · 보유 칸 = 보물 아이콘(납품 tre_* 없으면 기본 스탯별 팩 임시 tre_stat_* · 미보유 흰 25 % 실루엣) + 이름 띠 CardFrame01_Bottom · 등급 범례 칩 Round10 · 효과 줄 칩
  상자 탭 = 포인트 띠 BannerFrame05 + 가는 주황 바 + 보상 상자 · 상자 카드 CardFrame03(상자 색 + 안쪽 + 광택) + 상자 300 + 뒤 금빛 · 등급 칩 + % 한 줄 · 화살표 아이콘 · 열기 Primary 노랑 + 보유 칩(없으면 회색) · 상자 행 ItemFrame 150 + 수량 칩 + 선택 FocusLine 깜빡
  상자 색 = 코드 상수(ChestColor) → 테마 chest_<id> · 칸 색 chest_<id>_bg/deco/light/glow(C45)
- 연무장: 목록 바탕 · 카드 = 일러 자리(픽토그램 256 흰 25 % · 임시) + 요약 한 줄 + 어두운 판 칩 2줄(입장 아이콘 + 값 · 보상 칸 76) + 상태 칩 · 규칙 문장 → ⓘ 팝업 · 적장 토벌 「이벤트」 뱃지
- 내정 순찰: 목록 바탕 · 서브탭 · 섹션 「순찰」(레벨형 게이지 군량색 + 배지 「7h」) · 「시간당 수확」 스탯 칸 4(아이콘 64 · 값 44 Sen) · 「쌓인 보상」 타일 4 + 수량 칩 · 수확 Primary 녹 · 광고 2배 Secondary 하늘 + 횟수 · 즉시 Secondary 보라 + 보옥 비용
- 전투 HUD: 유닛 판(초상 원 마스크 + 이름 · 병종 원 · 체력 바 색 단계 · 공/방 칩 · 슬링/사거리 칩 · 상태) · 수 칩 = 아군/적 아이콘 + 숫자 · 스킬 사용 가능 = 뒤 금빛 호흡 · 기본 조작 문장 삭제(상황 안내 유지)
- 공통: Sen 에 「×」 글리프가 없어 수량은 「x」 · 회색 버튼 글자 흰 외곽선 · 금빛 틴트 α 1(Glow_Circle01 가운데 α 0.5 실측)
- 테스트 117/117 · console error 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-09 18:12:53 +09:00
parent ca8b4cc5e0
commit 4f22c4e091
8 changed files with 558 additions and 36 deletions

View File

@ -429,7 +429,7 @@ skin,btn_s_gray,Button/Button01_s_Gray.png,,2.1,,,,Secondary 회색 h56(비활
color,row_label_strip,,,,,,#FFFFFF0A,편성 라벨 칸 세로 띠(흰 4 %)
font,title,Assets/UI/GUIPro/ResourcesData/Fonts/Sen-ExtraBold.ttf,,,,,,제목·숫자(라틴 Sen · 한글 = 폴백 Malgun Gothic · PD 결정 전)
color,text_on_yellow,,,,,,#1E1E2A,노랑 Primary 위 글자(Fable §5-8)
color,ribbon_glow,,,,,,#F3B72EB3,결과 리본 뒤 금빛(α 0.7 · Fable §4-2 α 0.35 는 어두운 덮개 위에서 안 보여 올림)
color,ribbon_glow,,,,,,#FFD24AFF,결과 리본 뒤 금빛(그림 자체가 가운데 α 0.5 라 틴트 α 1 · Fable §4-2 α 0.35 는 안 보임 실측)
color,row_on_field,,,,,,#3F4262,필드 위 행 판(필드보다 밝게 · 팝업 교환 행)
skin,btn_l_yellow,Button/Button01_l_Yellow.png,,1.3,,,,Primary 노랑 h110(확인·10연·열기 · Fable §5-5)
skin,btn_l_red,Button/Button01_l_White_Bg.png,Button/Button01_l_White_Light1.png,1.35,5;5;5;18,0.35,,Primary 붉은 h104(출정 · 틴트 red)
@ -473,9 +473,9 @@ skin,card3_inner,Frame/CardFrame03_n_InnerBg.png,,1,,,,상자 카드 안쪽
skin,card3_light,Frame/CardFrame03_n_Light.png,,1,,,,상자 카드 광택
color,card3_inner,,,,,,#00000040,상자 카드 안쪽(검정 25 %)
color,card3_light,,,,,,#FFFFFF80,상자 카드 광택(흰 50 %)
color,chest_glow,,,,,,#F3B72E73,상자 뒤 빛(α 0.45)
color,chest_glow,,,,,,#FFD24AE6,상자 뒤 빛(그림 가운데 α 0.5 × 0.9)
skin,banner_point,Frame/BannerFrame05_Single.png,,3,,,,상자 포인트 띠(h90 · 장식 비율 유지)
color,banner_point,,,,,,#2C2F4A,상자 포인트 띠 틴트
color,banner_point,,,,,,#FFFFFF,상자 포인트 띠(그림 원색 · 왼쪽 어두운 칸 + 사선)
skin,bar4_bg,Slider/Slider_Basic04_Bg.png,,1.5,,,,가는 진행 바 바탕
skin,bar4_fill,Slider/Slider_Basic04_Fill_Orange.png,,1.5,,,,가는 진행 바 채움
icon,tre_stat_hp,Icon_ItemIcons/128/ItemIcon_Heart_Red.Png,,,,,,보물 임시 아이콘 체력(최종 = 보물 그림 N3)
@ -484,3 +484,12 @@ icon,tre_stat_def,Icon_ItemIcons/128/ItemIcon_Gear_Armor_Top.Png,,,,,,보물 임
icon,tre_stat_crit,Icon_ItemIcons/128/ItemIcon_Damage.Png,,,,,,보물 임시 아이콘 치명
icon,tre_stat_range,Icon_ItemIcons/128/ItemIcon_Gear_Boots.Png,,,,,,보물 임시 아이콘 사거리
icon,tre_stat_splash,Icon_ItemIcons/128/ItemIcon_Battle.Png,,,,,,보물 임시 아이콘 광역
icon,arena_art_0,Icon_PictoIcons/256/PictoIcon_Castle.Png,,,,,,연무장 카드 일러 자리 탑(임시 · 일러 N4)
icon,arena_art_1,Icon_PictoIcons/256/PictoIcon_Chest.Png,,,,,,연무장 카드 일러 자리 던전(임시)
icon,arena_art_2,Icon_PictoIcons/256/PictoIcon_Battle.Png,,,,,,연무장 카드 일러 자리 투기장(임시)
icon,arena_art_3,Icon_PictoIcons/256/PictoIcon_Boss.Png,,,,,,연무장 카드 일러 자리 적장 토벌(임시)
color,arena_art,,,,,,#FFFFFF40,연무장 카드 일러 자리 픽토그램(흰 25 %)
icon,hud_ally,Icon_PictoIcons/128/PictoIcon_Helmet_1.Png,,,,,,전투 HUD 아군 수 아이콘(Fable §4-8)
icon,hud_enemy,Icon_PictoIcons/128/PictoIcon_Boss.Png,,,,,,전투 HUD 적 수 아이콘
color,skill_glow,,,,,,#FFD24AFF,스킬 버튼 뒤 빛(사용 가능 · 금빛 · 붉은 버튼·모래 바탕과 대비)
icon,shield,IconMisc/Icon_Shield.png,,,,,,방어 스탯(Fable §5-8)

1 kind id path overlay ppu inset alpha color note
429 color row_label_strip #FFFFFF0A 편성 라벨 칸 세로 띠(흰 4 %)
430 font title Assets/UI/GUIPro/ResourcesData/Fonts/Sen-ExtraBold.ttf 제목·숫자(라틴 Sen · 한글 = 폴백 Malgun Gothic · PD 결정 전)
431 color text_on_yellow #1E1E2A 노랑 Primary 위 글자(Fable §5-8)
432 color ribbon_glow #F3B72EB3 #FFD24AFF 결과 리본 뒤 금빛(α 0.7 · Fable §4-2 α 0.35 는 어두운 덮개 위에서 안 보여 올림) 결과 리본 뒤 금빛(그림 자체가 가운데 α 0.5 라 틴트 α 1 · Fable §4-2 α 0.35 는 안 보임 실측)
433 color row_on_field #3F4262 필드 위 행 판(필드보다 밝게 · 팝업 교환 행)
434 skin btn_l_yellow Button/Button01_l_Yellow.png 1.3 Primary 노랑 h110(확인·10연·열기 · Fable §5-5)
435 skin btn_l_red Button/Button01_l_White_Bg.png Button/Button01_l_White_Light1.png 1.35 5;5;5;18 0.35 Primary 붉은 h104(출정 · 틴트 red)
473 skin card3_light Frame/CardFrame03_n_Light.png 1 상자 카드 광택
474 color card3_inner #00000040 상자 카드 안쪽(검정 25 %)
475 color card3_light #FFFFFF80 상자 카드 광택(흰 50 %)
476 color chest_glow #F3B72E73 #FFD24AE6 상자 뒤 빛(α 0.45) 상자 뒤 빛(그림 가운데 α 0.5 × 0.9)
477 skin banner_point Frame/BannerFrame05_Single.png 3 상자 포인트 띠(h90 · 장식 비율 유지)
478 color banner_point #2C2F4A #FFFFFF 상자 포인트 띠 틴트 상자 포인트 띠(그림 원색 · 왼쪽 어두운 칸 + 사선)
479 skin bar4_bg Slider/Slider_Basic04_Bg.png 1.5 가는 진행 바 바탕
480 skin bar4_fill Slider/Slider_Basic04_Fill_Orange.png 1.5 가는 진행 바 채움
481 icon tre_stat_hp Icon_ItemIcons/128/ItemIcon_Heart_Red.Png 보물 임시 아이콘 체력(최종 = 보물 그림 N3)
484 icon tre_stat_crit Icon_ItemIcons/128/ItemIcon_Damage.Png 보물 임시 아이콘 치명
485 icon tre_stat_range Icon_ItemIcons/128/ItemIcon_Gear_Boots.Png 보물 임시 아이콘 사거리
486 icon tre_stat_splash Icon_ItemIcons/128/ItemIcon_Battle.Png 보물 임시 아이콘 광역
487 icon arena_art_0 Icon_PictoIcons/256/PictoIcon_Castle.Png 연무장 카드 일러 자리 탑(임시 · 일러 N4)
488 icon arena_art_1 Icon_PictoIcons/256/PictoIcon_Chest.Png 연무장 카드 일러 자리 던전(임시)
489 icon arena_art_2 Icon_PictoIcons/256/PictoIcon_Battle.Png 연무장 카드 일러 자리 투기장(임시)
490 icon arena_art_3 Icon_PictoIcons/256/PictoIcon_Boss.Png 연무장 카드 일러 자리 적장 토벌(임시)
491 color arena_art #FFFFFF40 연무장 카드 일러 자리 픽토그램(흰 25 %)
492 icon hud_ally Icon_PictoIcons/128/PictoIcon_Helmet_1.Png 전투 HUD 아군 수 아이콘(Fable §4-8)
493 icon hud_enemy Icon_PictoIcons/128/PictoIcon_Boss.Png 전투 HUD 적 수 아이콘
494 color skill_glow #FFD24AFF 스킬 버튼 뒤 빛(사용 가능 · 금빛 · 붉은 버튼·모래 바탕과 대비)
495 icon shield IconMisc/Icon_Shield.png 방어 스탯(Fable §5-8)

View File

@ -502,7 +502,7 @@ MonoBehaviour:
- id: text_on_yellow
color: {r: 0.11764706, g: 0.11764706, b: 0.16470589, a: 1}
- id: ribbon_glow
color: {r: 0.9529412, g: 0.7176471, b: 0.18039216, a: 0.7019608}
color: {r: 1, g: 0.8235294, b: 0.2901961, a: 1}
- id: row_on_field
color: {r: 0.24705882, g: 0.25882354, b: 0.38431373, a: 1}
- id: banner_pattern
@ -562,9 +562,13 @@ MonoBehaviour:
- id: card3_light
color: {r: 1, g: 1, b: 1, a: 0.5019608}
- id: chest_glow
color: {r: 0.9529412, g: 0.7176471, b: 0.18039216, a: 0.4509804}
color: {r: 1, g: 0.8235294, b: 0.2901961, a: 0.9019608}
- id: banner_point
color: {r: 0.17254902, g: 0.18431373, b: 0.2901961, a: 1}
color: {r: 1, g: 1, b: 1, a: 1}
- id: arena_art
color: {r: 1, g: 1, b: 1, a: 0.2509804}
- id: skill_glow
color: {r: 1, g: 0.8235294, b: 0.2901961, a: 1}
skins:
- id: card
pack: {fileID: 21300000, guid: 3070a6f23e6ca4273897acf1b7e8c5ce, type: 3}
@ -1729,6 +1733,34 @@ MonoBehaviour:
pack: {fileID: 21300000, guid: d031f4b21493c4841914d4257119a4e3, type: 3}
samguk: {fileID: 0}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: arena_art_0
pack: {fileID: 21300000, guid: e63f49ada5fda4cc59b8b5080feeb0b7, type: 3}
samguk: {fileID: 0}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: arena_art_1
pack: {fileID: 21300000, guid: 304fa5e62cade424d9ae15c73cd20a12, type: 3}
samguk: {fileID: 0}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: arena_art_2
pack: {fileID: 21300000, guid: e0df45813c53b41d386a33c49657561d, type: 3}
samguk: {fileID: 0}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: arena_art_3
pack: {fileID: 21300000, guid: cd6dc5dd342ab486b96c3d3e666c6bb9, type: 3}
samguk: {fileID: 0}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: hud_ally
pack: {fileID: 21300000, guid: 6fcf7bcb2b29e4b589e82ab74f31c79e, type: 3}
samguk: {fileID: 0}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: hud_enemy
pack: {fileID: 21300000, guid: 0127e486ba6e6408ba9139509d699e4f, type: 3}
samguk: {fileID: 0}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: shield
pack: {fileID: 21300000, guid: 09135c198f8744fcd89493b169d11bd8, type: 3}
samguk: {fileID: 0}
tint: {r: 1, g: 1, b: 1, a: 1}
- id: tile_bridge
pack: {fileID: 0}
samguk: {fileID: 21300000, guid: 8e5bf7eb1e300474785ff2781eeb5093, type: 3}

View File

@ -25,6 +25,7 @@ namespace TKS.OutGame
public override void Build()
{
if (UISkin.On) UiKit.Paint(UiKit.Fill("ListBg", Rt), UiKit.Col("list_bg", UiKit.Bg), false); // 목록형 바탕(Fable §5-3)
Header("연무장", "탑 · 일일 던전 · 투기장 · 적장 토벌", true);
root = Body();
}
@ -83,6 +84,11 @@ namespace TKS.OutGame
{ "주간 보스(여포·조조 …) · 누적 피해", "보물 명장 상자", "하루 " + e.RaidDaily, "이번 주 " + boss + " · 누적 " + d.arena.raidDamage.ToString("N0") },
};
float w = 508f, h = 700f;
if (UISkin.Has("card4_bg") && UISkin.Has("btn_l_yellow"))
{
CardsKit(text, w, h);
return;
}
for (int i = 0; i < 4; i++)
{
int k = i;
@ -124,6 +130,81 @@ namespace TKS.OutGame
}
}
// 카드 그림 · 요약 · 보상 아이콘(테마 id · Fable §4-6 · 일러 N4 전까지 픽토그램 임시)
static readonly string[] Arts = { "arena_art_0", "arena_art_1", "arena_art_2", "arena_art_3" };
static readonly string[] Briefs = { "층마다 고정 편성 · 10층마다 보스", "요일 순환 재화 던전", "비동기 5v5 · 주간 랭킹", "주간 보스 · 누적 피해" };
static readonly string[][] RewardIcons =
{
new[] { "cur_gem", "cur_blueprint", "chest_gold" },
new[] { "cur_gold", "cur_iron", "cur_skillbook" },
new[] { "gear_flag", "cur_shard" },
new[] { "chest_general" },
};
static readonly string[] EntryIcons = { "cur_ticket", null, "sword", "fire" };
/// <summary>
/// 모드 카드(Fable 제안서 §4-6 · #1104) = CardFrame04 카드색 + 리본 제목(유지) · 위 일러 자리(픽토그램 220 흰 25 %) + 요약 한 줄 ·
/// 아래 어두운 판에 칩 2줄(입장 = 아이콘 + 값 · 보상 = 아이템 칸 72) · 상태 칩 · 규칙 문장 → ⓘ 팝업 · 이벤트 = 오른쪽 위 뱃지.
/// </summary>
void CardsKit(string[,] text, float w, float h)
{
var d = S.Save;
var db = S.Db;
var now = S.Now;
string[] entry =
{
"입장권 " + d.arena.towerTickets + " / " + db.Economy.TowerTicketsDaily,
"오늘 " + OpenCount(now) + "곳 · 각 " + (db.Dungeons.dungeons.Count > 0 ? db.Dungeons.dungeons[0].entriesPerDay : 0) + "회",
"남은 " + ArenaRules.ArenaLeft(db, d, now) + " / " + db.Arena.EntriesPerDay,
"하루 " + db.Economy.RaidDaily + "회",
};
var soft = UiKit.Col("text_soft", UiKit.InkSoft);
for (int i = 0; i < 4; i++)
{
int k = i;
float x = 24f + (i % 2) * (w + 16f);
float y = 18f + (i / 2) * (h + 20f);
var ci = UiKit.Skin(UiKit.At(root, "Card" + i, x, y, w, h), "card4_bg", UiKit.Col("arena_" + i, UiKit.Paper), true);
var card = ci.transform;
card.gameObject.AddComponent<Button>().onClick.AddListener(() => SetPage(k + 1));
UiKit.Layer(card, "Glow", "card4_glow", UiKit.Col("arena_glow", UiKit.White), 0f, 0f, 0f, h * 0.5f);
// 일러 자리 = 픽토그램(흰 25 %)
var art = UiKit.Icon(card, Arts[i], new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -232f), 220f);
if (art != null) art.color = UiKit.Col("arena_art", new Color(1f, 1f, 1f, 0.25f));
var brief = UiKit.Title(UiKit.Label(card, Briefs[i], 28, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -356f), new Vector2(w - 40f, 44f)), true);
brief.horizontalOverflow = HorizontalWrapMode.Overflow;
// 칩 2줄 판
var board = UiKit.Skin(UiKit.At(card, "Inner", 14f, 414f, w - 28f, 182f), "panel12", UiKit.Col("arena_inner", UiKit.Paper)).transform;
UiKit.Label(board, "입장", 26, soft, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(20f, -14f), new Vector2(80f, 56f));
UiKit.InfoChip(board, "Entry", 96f, 16f, w - 28f - 96f - 18f, 52f, EntryIcons[i], entry[i], 26);
UiKit.Label(board, "보상", 26, soft, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(20f, -98f), new Vector2(80f, 72f));
for (int r = 0; r < RewardIcons[i].Length; r++)
UiKit.RewardTile(board, "Rw" + r, 96f + r * 86f, 92f, 76f, "gear_white_", RewardIcons[i][r], null, false);
// 상태 칩
var st = UiKit.Skin(UiKit.At(card, "Status", 16f, h - 86f, w - 32f, 66f), "chip", UiKit.Col("chip_dark", UiKit.Dark));
var stt = UiKit.Title(UiKit.FillLabel(st.transform, text[i, 3], 26, UiKit.Col("lv_fill", UiKit.Coin), TextAnchor.MiddleCenter, true, 16f), true);
stt.horizontalOverflow = HorizontalWrapMode.Overflow;
// 리본 제목(맨 위) · 이벤트 뱃지 · ⓘ
var rib = UiKit.Skin(UiKit.Rect("Ribbon", card, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, 4f), new Vector2(w - 40f, 96f)), Ribbons[i], UiKit.White);
var rt = UiKit.Title(UiKit.FillLabel(rib.transform, ModeNames[i], 42, UiKit.White, TextAnchor.MiddleCenter, true, 40f), true);
rt.rectTransform.offsetMin = new Vector2(rt.rectTransform.offsetMin.x, 16f);
if (i == 3)
{
var ev = UiKit.Icon(card, "badge_hot", new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), new Vector2(-30f, -118f), 96f);
if (ev != null)
{
UiKit.DropShadow(ev, 3f);
UiKit.Title(UiKit.FillLabel(ev.transform, "이벤트", 22, UiKit.White), true);
}
}
var info = UiKit.Rect("Info", card, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(46f, -122f), new Vector2(60f, 60f));
UiKit.Skin(info, "badge_circle", UiKit.Col("chip_dark", UiKit.Dark), true);
if (UiKit.Icon(info, "info", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 32f) == null) UiKit.Title(UiKit.FillLabel(info, "i", 30, UiKit.White), true);
string rules = "<b>규칙</b> " + text[i, 0] + "\n\n<b>보상</b> " + text[i, 1] + "\n\n<b>입장</b> " + text[i, 2];
info.gameObject.AddComponent<Button>().onClick.AddListener(() => Root.Modal(ModeNames[k], rules, new OutGameRoot.ModalButton("확인", UiKit.Dark, null)));
}
}
int OpenCount(System.DateTime now)
{
int n = 0;

View File

@ -61,12 +61,15 @@ namespace TKS.OutGame
public override void Build()
{
Tab = 2;
if (UISkin.On) UiKit.Paint(UiKit.Fill("ListBg", Rt), UiKit.Col("list_bg", UiKit.Bg), false); // 목록형 바탕(Fable §5-3)
Header("내정", "훈련 · 순찰 · 개발", true);
var body = Body();
for (int i = 0; i < PageNames.Length; i++)
{
int k = i;
tabs.Add(UiKit.Btn("Tab" + i, body, PageNames[i], 36, UiKit.Paper, UiKit.Ink, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f + i * 348f, -18f), new Vector2(332f, 84f), () => SetPage(k)));
tabs.Add(UISkin.On
? UiKit.SubTab(body, "Tab" + i, PageNames[i], 24f + i * 348f, 18f, 332f, 84f, () => SetPage(k))
: UiKit.Btn("Tab" + i, body, PageNames[i], 36, UiKit.Paper, UiKit.Ink, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f + i * 348f, -18f), new Vector2(332f, 84f), () => SetPage(k)));
pages.Add(UiKit.Fill("Page" + i, body, 0f, 0f, 118f, 0f));
}
}
@ -95,13 +98,21 @@ namespace TKS.OutGame
for (int i = 0; i < tabs.Count; i++)
{
bool on = i == page;
if (UISkin.On) UiKit.SetSubTab(tabs[i], on);
else
{
tabs[i].GetComponent<Image>().color = on ? UiKit.Red : UiKit.Paper; // 선택 = 붉은 옻칠(다른 화면 탭과 같음)
UiKit.LabelOf(tabs[i]).color = on ? UiKit.White : UiKit.Ink;
}
pages[i].gameObject.SetActive(on);
}
UiKit.Clear(pages[page]);
if (page == 0) BuildTraining(pages[0]);
else if (page == 1) BuildPatrol(pages[1]);
else if (page == 1)
{
if (UISkin.Has("panel_field") && UISkin.Has("btn_l_green")) BuildPatrolKit(pages[1]);
else BuildPatrol(pages[1]);
}
else BuildDev(pages[2]);
}
@ -271,6 +282,91 @@ namespace TKS.OutGame
});
}
/// <summary>
/// 순찰(Fable 제안서 §4-7 · #1104) — 섹션 「순찰」(기준 줄 + 레벨형 게이지 군량색 + 배지 「7h」) → 「시간당 수확」 스탯 칸 4(아이콘 64 · 라벨 · 값 Sen) →
/// 「쌓인 보상」 타일 4(ItemFrame 132 + 수량 칩) → 수확 Primary 녹 · 광고 2배 Secondary 하늘 + 광고 글리프 · 즉시 Secondary 보라 + 보옥 비용.
/// </summary>
void BuildPatrolKit(RectTransform p)
{
var db = S.Db;
var d = S.Save;
var e = db.Economy;
var row = PatrolRules.Row(db, d);
float cap = PatrolRules.CapHours(db, d);
float h = PatrolRules.Hours(db, d, S.Now);
var soft = UiKit.Col("text_soft", UiKit.InkSoft);
const float W = 1032f, Fw = W - 12f, GaugeBar = 640f;
// ① 순찰
RectTransform ar;
var a = UiKit.SectionPanel(p, "Patrol", 24f, 6f, W, 236f, false, false, out ar);
UiKit.SectionTitle(ar, "순찰");
var basis = UiKit.Label(ar, "기준 = 마지막 클리어 " + (row != null ? row.chapter : 0) + "장 · 오프라인 누적", 26, soft, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -12f), new Vector2(620f, 48f), false);
basis.horizontalOverflow = HorizontalWrapMode.Overflow;
Text badge;
var fill = UiKit.Gauge(a, "Gauge", 16f, 66f, GaugeBar, "lv_badge", "grain", out badge);
UiKit.SetGauge(fill, cap > 0f ? h / cap : 0f, GaugeBar);
badge.text = Mathf.FloorToInt(h) + "h";
badge.fontSize = 30;
var cur = UiKit.Title(UiKit.Label(a, FormatHours(h) + " / " + FormatHours(cap), 30, UiKit.White, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -84f), new Vector2(220f, 60f)), true);
cur.horizontalOverflow = HorizontalWrapMode.Overflow;
UiKit.Label(a, "최대 " + FormatHours(cap) + " · 성장 패스 24시간(준비 중) · 농업 개발 +" + Mathf.RoundToInt(DevRules.Mod(db, d, DevRules.PatrolCap)) + "분", 24, soft, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -168f), new Vector2(Fw - 48f, 44f), false);
// ② 시간당 수확
RectTransform br;
var b = UiKit.SectionPanel(p, "Rate", 24f, 258f, W, 448f, false, false, out br);
UiKit.SectionTitle(br, "시간당 수확");
string[] rn = { "골드", "병사 경험", "승급 장비", "보물 청동 상자" };
string[] rv =
{
PatrolRules.GoldPerHour(db, d).ToString("N0"),
PatrolRules.ExpPerHour(db, d).ToString("N0"),
PatrolRules.GearPerHour(db, d).ToString("0.##"),
PatrolRules.ChestPerHour(db, d).ToString("0.##"),
};
string[] rs = { "/ 시간", "/ 시간 · 출전 편성", "/ 시간 · " + UiKit.GradeKo(PatrolRules.GearGrade(db, d)) + " 등급", "/ 시간" };
string[] rIcon = { "cur_gold", "cur_exp", "cur_gear", "chest_bronze" };
for (int i = 0; i < 4; i++)
{
var cell = UiKit.Skin(UiKit.At(b, "Row" + i, 14f, 76f + i * 90f, Fw - 28f, 82f), "panel12", UiKit.Col("row_on_field", UiKit.Paper)).transform;
var ic = UiKit.Icon(cell, rIcon[i], new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(50f, 0f), 64f);
if (ic != null) UiKit.DropShadow(ic, 3f);
UiKit.Label(cell, rn[i], 30, soft, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(98f, 0f), new Vector2(300f, 60f), false);
var sub = UiKit.Label(cell, rs[i], 24, soft, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-24f, -2f), new Vector2(300f, 50f), false);
sub.horizontalOverflow = HorizontalWrapMode.Overflow;
var val = UiKit.Title(UiKit.Label(cell, rv[i], 44, UiKit.White, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-24f - Mathf.Ceil(sub.preferredWidth) - 14f, 2f), new Vector2(260f, 70f)), true);
val.horizontalOverflow = HorizontalWrapMode.Overflow;
}
// ③ 쌓인 보상
var y = PatrolRules.Preview(db, d, h, 1f);
RectTransform cr;
var c = UiKit.SectionPanel(p, "Stored", 24f, 722f, W, 262f, false, false, out cr);
UiKit.SectionTitle(cr, "쌓인 보상");
string[] sn = { y.gold.ToString("N0"), y.exp.ToString("N0"), y.gear.ToString(), y.chests.ToString() };
const float Tile = 132f, Gap = 64f;
float tx0 = (Fw - 4f * Tile - 3f * Gap) * 0.5f;
for (int i = 0; i < 4; i++)
UiKit.RewardTile(c, "Tile" + i, tx0 + i * (Tile + Gap), 90f, Tile, i == 2 ? "gear_" + PatrolRules.GearGrade(db, d) + "_" : "gear_white_", rIcon[i], sn[i], i == 2);
// ④ 버튼
EconomyRules.RollDaily(d, S.Now);
var hb = UiKit.SkinBtn(p, "Harvest", h > 0f ? "btn_l_green" : "btn_gray", UiKit.White, "수확", 52, UiKit.Col("text_on_green", UiKit.White), (1080f - 600f) * 0.5f, 1004f, 600f, 128f, () => DoHarvest(false));
hb.interactable = h > 0f;
bool adOk = h > 0f && d.adsUsed.patrol < e.PatrolAdDaily;
var ad = UiKit.SkinBtn(p, "Ad", adOk ? "btn_sky" : "btn_gray", UiKit.White, "광고 2배", 38, UiKit.White, 24f, 1152f, 505f, 112f, () => DoHarvest(true));
ad.interactable = adOk;
UiKit.BtnCost(ad, "ad", d.adsUsed.patrol + "/" + e.PatrolAdDaily, 34f, 28);
var ins = UiKit.SkinBtn(p, "Instant", "btn_purple", UiKit.White, "즉시 " + FormatHours(e.PatrolInstantHours), 38, UiKit.White, 1056f - 505f, 1152f, 505f, 112f, () =>
{
string why;
var r = S.InstantHarvest(out why);
if (r == null) Root.Toast(why);
else ShowYield("즉시 수확", r);
});
UiKit.BtnCost(ins, "cur_gem", e.PatrolInstantGem.ToString(), 40f, 32);
}
void DoHarvest(bool ad)
{
string why;

View File

@ -39,16 +39,32 @@ namespace TKS.OutGame
public override void Build()
{
Tab = 1;
if (UISkin.On) UiKit.Paint(UiKit.Fill("ListBg", Rt), UiKit.Col("list_bg", UiKit.Bg), false); // 목록형 바탕(Fable §5-3)
Header("보물", "", true); // 부제 삭제(설계서 §2-5)
var body = Body();
for (int i = 0; i < PageNames.Length; i++)
{
int k = i;
tabs.Add(UiKit.Btn("Tab" + i, body, PageNames[i], 36, UiKit.Paper, UiKit.Ink, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f + i * 348f, -18f), new Vector2(332f, 84f), () => SetPage(k)));
tabs.Add(UISkin.On
? UiKit.SubTab(body, "Tab" + i, PageNames[i], 24f + i * 348f, 18f, 332f, 84f, () => SetPage(k))
: UiKit.Btn("Tab" + i, body, PageNames[i], 36, UiKit.Paper, UiKit.Ink, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f + i * 348f, -18f), new Vector2(332f, 84f), () => SetPage(k)));
pages.Add(UiKit.Fill("Page" + i, body, 0f, 0f, 118f, 0f));
}
}
/// <summary>새 상자 화면 부품(Fable §4-5)이 테마에 있는가.</summary>
static bool Kit
{
get { return UISkin.Has("card3_bg") && UISkin.Has("banner_point") && UISkin.Has("btn_l_yellow"); }
}
/// <summary>보물 아이콘 id — 납품 그림(tre_&lt;id&gt;)이 있으면 그것 · 없으면 기본 스탯별 팩 임시 아이콘(tre_stat_* · Fable §6 N3 전까지).</summary>
static string IconId(TreasureDb.Item it)
{
if (UiKit.HasFinalIcon("tre_" + it.id)) return "tre_" + it.id;
return UISkin.Icon("tre_stat_" + it.baseStat) != null ? "tre_stat_" + it.baseStat : null;
}
public void SetPage(int p)
{
page = Mathf.Clamp(p, 0, pages.Count - 1);
@ -67,13 +83,18 @@ namespace TKS.OutGame
for (int i = 0; i < tabs.Count; i++)
{
bool on = i == page;
if (UISkin.On) UiKit.SetSubTab(tabs[i], on);
else
{
tabs[i].GetComponent<Image>().color = on ? UiKit.Red : UiKit.Paper; // 선택 = 붉은 옻칠(다른 화면 탭과 같음)
UiKit.LabelOf(tabs[i]).color = on ? UiKit.White : UiKit.Ink;
}
pages[i].gameObject.SetActive(on);
}
UiKit.Clear(pages[page]);
if (page == 0) BuildOwned(pages[0]);
else if (page == 1) BuildSets(pages[1]);
else if (Kit) BuildChestsKit(pages[2]);
else BuildChests(pages[2]);
}
@ -95,10 +116,12 @@ namespace TKS.OutGame
cell.sprite = null; // 바닥 = 클릭 판(투명)
var t = cell.transform;
UiKit.ItemSlot(t, owned ? "tre_" + it.grade + "_" : "tre_none_", owned);
var art = UiKit.Rect("Art", t, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -96f), new Vector2(140f, 140f));
if (UiKit.HasFinalIcon("tre_" + it.id))
var art = UiKit.Rect("Art", t, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -100f), new Vector2(140f, 140f));
string icon = IconId(it);
if (icon != null)
{
var ic = UiKit.Icon(art, "tre_" + it.id, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 140f);
// 아이콘 = 납품 그림 또는 기본 스탯별 임시(Fable §4-5 「글자 → 보물 아이콘」) · 미보유 = 흰 25 % 실루엣
var ic = UiKit.Icon(art, icon, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, icon.StartsWith("tre_stat_") ? 120f : 140f);
if (owned) UiKit.DropShadow(ic, 5f);
else ic.color = UiKit.Col("tre_none_icon", UiKit.InkSoft);
}
@ -115,8 +138,16 @@ namespace TKS.OutGame
}
else if (st.star > 0) UiKit.StarRow(t, st.star, st.star, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 72f), 30f);
}
var band = UiKit.Skin(UiKit.Rect("Name", t, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 12f), new Vector2(206f, 52f)), "plate12", UiKit.Col("gear_plate", UiKit.Dark));
var nt = UiKit.FillLabel(band.transform, owned ? it.nameKo : "미보유", 26, owned ? UiKit.White : UiKit.InkSoft);
// 이름 띠 = 칸 아래에 붙은 카드 띠(CardFrame01_Bottom · Fable §4-5) · 테마가 없으면 둥근 판
var band = UISkin.Has("card_bottom")
? UiKit.Layer(t, "Name", "card_bottom", UiKit.Col("card_bottom", UiKit.Dark), 6f, 0f, 6f, 6f)
: UiKit.Skin(UiKit.Rect("Name", t, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 12f), new Vector2(206f, 52f)), "plate12", UiKit.Col("gear_plate", UiKit.Dark));
if (UISkin.Has("card_bottom"))
{
band.rectTransform.anchorMax = new Vector2(1f, 0f);
band.rectTransform.offsetMax = new Vector2(-6f, 62f);
}
var nt = UiKit.FillLabel(band.transform, owned ? it.nameKo : "미보유", 26, owned ? UiKit.White : UiKit.Col("text_soft", UiKit.InkSoft));
nt.horizontalOverflow = HorizontalWrapMode.Overflow;
}
@ -128,12 +159,17 @@ namespace TKS.OutGame
int owned = 0;
foreach (var it in db.Treasures.items)
if (S.Save.inv.Treasure(it.id) != null) owned++;
UiKit.Label(p, "보유 " + owned + " / " + db.Treasures.items.Count, 36, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(28f, -8f), new Vector2(360f, 56f));
var cnt = UiKit.Title(UiKit.Label(p, "보유 " + owned + " / " + db.Treasures.items.Count, 36, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(28f, -8f), new Vector2(360f, 56f)), UISkin.On);
cnt.horizontalOverflow = HorizontalWrapMode.Overflow;
var grades = db.Economy.TreasureGrades;
for (int i = 0; i < grades.Count; i++)
{
var chip = UiKit.Box("G" + i, p, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f - (grades.Count - 1 - i) * 118f, -12f), new Vector2(110f, 48f), GradeColor(grades[i].Key));
UiKit.FillLabel(chip.transform, TreasureRules.GradeKo(grades[i].Key), 24, UiKit.White);
// 등급 범례 칩 = 서브탭 틀 소형(Round10 · Fable §4-5 「120×56 통일」) · 등급 대표 색
var pos = new Vector2(-24f - (grades.Count - 1 - i) * 126f, -8f);
var chip = UISkin.Has("subtab")
? UiKit.Skin(UiKit.Rect("G" + i, p, new Vector2(1f, 1f), new Vector2(1f, 1f), pos, new Vector2(118f, 56f)), "subtab", GradeColor(grades[i].Key))
: UiKit.Box("G" + i, p, new Vector2(1f, 1f), new Vector2(1f, 1f), pos, new Vector2(110f, 48f), GradeColor(grades[i].Key));
UiKit.Title(UiKit.FillLabel(chip.transform, TreasureRules.GradeKo(grades[i].Key), 26, UiKit.White), UISkin.On);
}
ScrollRect sr;
var grid = UiKit.ScrollGrid("Grid", p, 24f, 24f, 76f, 64f, new Vector2(240f, 290f), new Vector2(16f, 16f), 4, out sr);
@ -157,8 +193,15 @@ namespace TKS.OutGame
float v = TreasureRules.StatPct(db, S.Save, s);
if (v > 0f) bonus.Add(TreasureRules.StatKo(s) + " +" + v.ToString("0.#") + "%");
}
UiKit.Label(p, bonus.Count > 0 ? "전체 효과 " + string.Join(" · ", bonus.ToArray()) : "보물을 모으면 전 유닛에 상시 효과", 26, UiKit.InkSoft, TextAnchor.MiddleCenter,
new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 8f), new Vector2(1030f, 50f), false);
string fx = bonus.Count > 0 ? "전체 효과 " + string.Join(" · ", bonus.ToArray()) : "보물을 모으면 전 유닛에 상시 효과";
if (UISkin.Has("chip"))
{
// 아래 효과 줄 = 정보 칩(§5-6)
var c =UiKit.Skin(UiKit.Rect("Effects", p, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 4f), new Vector2(1032f, 54f)), "chip", UiKit.Col("chip_dark", UiKit.Dark));
var ft = UiKit.FillLabel(c.transform, fx, 26, UiKit.White, TextAnchor.MiddleCenter, false, 20f);
ft.horizontalOverflow = HorizontalWrapMode.Overflow;
}
else UiKit.Label(p, fx, 26, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 8f), new Vector2(1030f, 50f), false);
}
public void Detail(string id)
@ -317,6 +360,134 @@ namespace TKS.OutGame
}
}
/// <summary>
/// 상자 탭(Fable 제안서 §4-5 · #1104) — 포인트 띠(BannerFrame05 · 가는 주황 바 · 「85 / 120」 · 오른쪽 보상 상자) →
/// 상자 카드(CardFrame03 상자 색 + 안쪽 + 광택 · 상자 아이콘 300 + 뒤 금빛 · 등급 칩 + % 한 줄 · 내용물 줄) · 좌우 화살표 →
/// 「열기」 Primary 노랑 + 보유 칩(없으면 회색) → 아래 상자 종류 행(ItemFrame 5층 상자 색 + 수량 칩 · 선택 = FocusLine 깜빡).
/// </summary>
void BuildChestsKit(RectTransform p)
{
var db = S.Db;
var d = S.Save;
var e = db.Economy;
var soft = UiKit.Col("text_soft", UiKit.InkSoft);
// 포인트 띠
var band = UiKit.Skin(UiKit.At(p, "Points", 24f, 6f, 1032f, 92f), "banner_point", UiKit.Col("banner_point", UiKit.White)).transform;
var pi = UiKit.Icon(band, "chest_point", new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(46f, 2f), 76f);
if (pi != null) UiKit.DropShadow(pi, 3f);
var reward = db.Chests.Get(e.ChestPointReward);
UiKit.Label(band, Mathf.Max(0, e.ChestPointGoal - d.inv.chestPoints) + "포인트 더 모으면 " + (reward != null ? reward.nameKo : ""), 28, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(112f, -6f), new Vector2(640f, 44f));
var bg = UiKit.Skin(UiKit.At(band, "Bar", 112f, 50f, 600f, 30f), "bar4_bg", UiKit.White);
var fill = UiKit.Skin(UiKit.At(bg.transform, "Fill", 4f, 4f, 592f, 20f), "bar4_fill", UiKit.White).rectTransform;
float v = Mathf.Clamp01(d.inv.chestPoints / (float)Mathf.Max(1, e.ChestPointGoal));
fill.sizeDelta = new Vector2(Mathf.Max(16f, 592f * v), 20f);
fill.gameObject.SetActive(v > 0.001f);
var pt = UiKit.Title(UiKit.Label(band, d.inv.chestPoints + " / " + e.ChestPointGoal, 30, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(730f, -38f), new Vector2(180f, 50f)), true);
pt.horizontalOverflow = HorizontalWrapMode.Overflow;
if (reward != null)
{
var ri = UiKit.Icon(band, "chest_" + reward.id, new Vector2(1f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(-62f, 4f), 80f);
if (ri != null) UiKit.DropShadow(ri, 3f);
}
var c = db.Chests.Get(SelectedChest);
if (c == null) return;
int count = TreasureRules.ChestCount(d, c.id);
// 상자 카드
const float Cw = 660f, Ch = 680f, Cy = 120f;
var card = UiKit.At(p, "Card", (1080f - Cw) * 0.5f, Cy, Cw, Ch);
UiKit.Skin(card, "card3_bg", ChestColor(c.id));
UiKit.Layer(card, "Inner", "card3_inner", UiKit.Col("card3_inner", Color.clear), 18f, 104f, 18f, 18f);
UiKit.Layer(card, "Light", "card3_light", UiKit.Col("card3_light", Color.clear), 6f, 6f, 6f, 10f);
var name = UiKit.Title(UiKit.Label(card, c.nameKo, 56, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -16f), new Vector2(Cw - 40f, 80f)), true);
name.horizontalOverflow = HorizontalWrapMode.Overflow;
if (UISkin.Has("glow_circle"))
{
var glow = UiKit.Skin(UiKit.Rect("Glow", card, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -300f), new Vector2(520f, 520f)), "glow_circle", UiKit.Col("chest_glow", Color.clear));
var pulse = glow.gameObject.AddComponent<UiPulse>();
pulse.min = 0.7f;
pulse.period = 1.6f;
}
var ci = UiKit.Icon(card, "chest_" + c.id, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -300f), 300f);
if (ci != null) UiKit.DropShadow(ci, 8f);
var probs = new List<int>();
for (int i = 0; i < ChestDb.ProbGrades.Length; i++)
if (c.p[i] > 0f) probs.Add(i);
const float Pw = 178f, Pg = 14f;
float px = (Cw - probs.Count * Pw - (probs.Count - 1) * Pg) * 0.5f;
for (int k = 0; k < probs.Count; k++)
{
int i = probs[k];
string g = ChestDb.ProbGrades[i];
var chip = UiKit.Skin(UiKit.At(card, "P" + i, px + k * (Pw + Pg), 492f, Pw, 56f), "para_orange", GradeColor(g));
var ct = UiKit.Title(UiKit.FillLabel(chip.transform, TreasureRules.GradeKo(g) + " " + (c.p[i] * 100f).ToString("0.#") + "%", 28, UiKit.White), true);
ct.horizontalOverflow = HorizontalWrapMode.Overflow;
}
var extra = new List<string>();
if (c.goldMax > 0) extra.Add("골드 " + c.goldMin + "~" + c.goldMax);
if (c.gemMax > 0) extra.Add("보옥 " + c.gemMin + "~" + c.gemMax);
if (c.ironMax > 0) extra.Add("운철 " + c.ironMin + "~" + c.ironMax);
extra.Add("포인트 +" + c.points);
UiKit.Label(card, string.Join(" · ", extra.ToArray()), 26, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 40f), new Vector2(Cw - 60f, 50f), false);
// 좌우 화살표(카드 가운데 높이)
Arrow(p, "Prev", "arrow_prev", 30f, Cy + Ch * 0.5f - 70f, () => Step(-1));
Arrow(p, "Next", "arrow_next", 1080f - 30f - 100f, Cy + Ch * 0.5f - 70f, () => Step(1));
// 열기
var open = UiKit.SkinBtn(p, "Open", count > 0 ? "btn_l_yellow" : "btn_gray", UiKit.White, count > 0 ? "열기" : "상자 없음", 46,
count > 0 ? UiKit.Col("text_on_yellow", UiKit.Ink) : UiKit.White, (1080f - 460f) * 0.5f, Cy + Ch + 26f, 460f, 116f, () => DoOpen(SelectedChest));
open.interactable = count > 0;
if (count > 0) UiKit.BtnCost(open, "chest_" + c.id, "x" + count, 56f, 34);
UiKit.Label(p, "상자는 이 탭에서만 엽니다 · 중복 = 조각 · 천기 = 적장 토벌 전용", 26, soft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -(Cy + Ch + 156f)), new Vector2(1000f, 44f), false);
// 상자 종류 행(아래)
int n = db.Chests.chests.Count;
const float Tw = 150f;
float gap = n > 1 ? Mathf.Min(64f, (1032f - n * Tw) / (n - 1)) : 0f;
float tx0 = (1080f - n * Tw - (n - 1) * gap) * 0.5f;
float ty = OutGameRoot.BodyH - 118f - 24f - 40f - Tw;
for (int i = 0; i < n; i++)
{
var ch = db.Chests.chests[i];
int k = i;
bool sel = i == chestIdx;
var cell = UiKit.At(p, "Chest_" + ch.id, tx0 + i * (Tw + gap), ty, Tw, Tw);
UiKit.ItemSlot(cell, "chest_" + ch.id + "_", sel);
var hit = cell.gameObject.AddComponent<Image>();
hit.color = Color.clear;
cell.gameObject.AddComponent<Button>().onClick.AddListener(() =>
{
chestIdx = k;
Refresh();
});
var ic = UiKit.Icon(cell, "chest_" + ch.id, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 6f), 104f);
if (ic != null) UiKit.DropShadow(ic, 4f);
UiKit.QtyChip(cell, "x" + TreasureRules.ChestCount(d, ch.id), 28);
if (sel && UISkin.Has("focus_line"))
{
var f = UiKit.Layer(cell, "Focus", "focus_line", UiKit.Col("select_line", UiKit.Coin), -8f, -8f, -8f, -8f);
f.gameObject.AddComponent<UiPulse>();
}
var nl = UiKit.Label(p, ch.nameKo, 26, sel ? UiKit.White : soft, TextAnchor.UpperCenter, new Vector2(0f, 1f), new Vector2(0.5f, 1f), new Vector2(tx0 + i * (Tw + gap) + Tw * 0.5f, -(ty + Tw + 10f)), new Vector2(Tw + gap, 36f), sel);
nl.horizontalOverflow = HorizontalWrapMode.Overflow;
}
}
/// <summary>화살표 버튼(투명 판 + 팩 화살표 아이콘 · 없으면 글자).</summary>
static void Arrow(Transform p, string name, string icon, float x, float y, System.Action onClick)
{
var rt = UiKit.At(p, name, x, y, 100f, 140f);
var img = rt.gameObject.AddComponent<Image>();
img.color = Color.clear;
rt.gameObject.AddComponent<Button>().onClick.AddListener(() => onClick());
var ic = UiKit.Icon(rt, icon, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 96f);
if (ic != null) UiKit.DropShadow(ic, 4f);
else UiKit.Title(UiKit.FillLabel(rt, name == "Prev" ? "◁" : "▷", 70, UiKit.White), true);
}
void Step(int dir)
{
int n = Mathf.Max(1, S.Db.Chests.chests.Count);
@ -350,17 +521,10 @@ namespace TKS.OutGame
new OutGameRoot.ModalButton("확인", UiKit.Dark, null));
}
/// <summary>상자 대표 색 = 테마 chest_&lt;id&gt;(구 코드 상수 → ui_theme.csv · C45 · #1104).</summary>
public static Color ChestColor(string id)
{
switch (id)
{
case "bronze": return new Color(0.69f, 0.47f, 0.3f);
case "silver": return new Color(0.66f, 0.68f, 0.72f);
case "gold": return new Color(0.88f, 0.7f, 0.22f);
case "general": return new Color(0.62f, 0.25f, 0.22f);
case "gem": return new Color(0.3f, 0.65f, 0.57f);
}
return UiKit.Slot;
return UiKit.Col("chest_" + id, UiKit.Slot);
}
}
}

View File

@ -720,7 +720,7 @@ namespace TKS.OutGame
var bt = UiKit.Title(UiKit.Label(row, "+" + gain, 40, UiKit.Col("ok", UiKit.Ok), TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(ax + 112f, -22f), new Vector2(140f, 60f)), true);
bt.horizontalOverflow = HorizontalWrapMode.Overflow;
UiKit.Label(row, note, 24, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(30f, -96f), new Vector2(w - 380f, 36f), false);
var btn = UiKit.SkinBtn(row, "Btn", can || !UISkin.Has("btn_gray") ? skin : "btn_gray", UiKit.White, label, 38, can ? UiKit.White : UiKit.Col("disabled_text", UiKit.Disabled), w - Pad * 2f - 24f - 250f, 20f, 250f, 100f, onClick);
var btn = UiKit.SkinBtn(row, "Btn", can || !UISkin.Has("btn_gray") ? skin : "btn_gray", UiKit.White, label, 38, UiKit.White, w - Pad * 2f - 24f - 250f, 20f, 250f, 100f, onClick);
if (skin == "btn_s_green" && can) UiKit.BtnCost(btn, "ad", "", 36f, 24);
}
@ -797,7 +797,7 @@ namespace TKS.OutGame
ti.color = UiKit.White;
UiKit.DropShadow(ti, 2f);
}
var n = UiKit.Title(UiKit.Label(field, "×" + counts[kinds[k]], 36, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(cx + 50f, -y - 94f), new Vector2(100f, 50f)), true);
var n = UiKit.Title(UiKit.Label(field, "x" + counts[kinds[k]], 36, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(cx + 50f, -y - 94f), new Vector2(100f, 50f)), true);
n.horizontalOverflow = HorizontalWrapMode.Overflow;
}
y += 140f;

View File

@ -358,7 +358,7 @@ namespace TKS.OutGame
{
float h = size + 12f;
var chip = Skin(Rect("Qty", cell, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(8f, -8f), new Vector2(60f, h)), "chip", Col("chip_dark", Dark));
var t = Title(FillLabel(chip.transform, qty, size, White, TextAnchor.MiddleCenter, true, 0f), true);
var t = Title(FillLabel(chip.transform, qty.Replace("×", "x"), size, White, TextAnchor.MiddleCenter, true, 0f), true); // Sen 에 × 글리프 없음
t.horizontalOverflow = HorizontalWrapMode.Overflow;
chip.rectTransform.sizeDelta = new Vector2(Mathf.Max(h + 8f, Mathf.Ceil(t.preferredWidth) + 30f), h);
return t;

View File

@ -38,6 +38,27 @@ namespace TKS
Text skillPassive;
Text skillLog;
// 유닛 정보 판 · 수 칩 · 스킬 빛(Fable 제안서 §4-8 · #1104 · 테마가 있을 때만)
RectTransform unitCard;
RectTransform unitBody;
Image portraitRing;
Image portraitArt;
Image typeRing;
Image typeIcon;
RectTransform hpFill;
Text nameText;
Text hpText;
Text atkText;
Text defText;
Text extraText;
Text stateText;
Text emptyText;
Unit shownUnit;
Text allyCount;
Text enemyCount;
Image skillGlow;
const float HpBarW = 360f;
Image minimapBg;
Image minimapView;
readonly Dictionary<Unit, Image> minimapDots = new Dictionary<Unit, Image>();
@ -160,6 +181,8 @@ namespace TKS
var mmMid = MakeImage("Mid", minimapBg.transform, new Vector2(0f, 0.5f), new Vector2(1f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(0f, 2f), new Color(1f, 1f, 1f, 0.25f));
minimapView = MakeImage("View", minimapBg.transform, Vector2.zero, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(40f, 60f), new Color(1f, 1f, 1f, 0.2f));
if (UISkin.Has("hp_bg") && UISkin.Has("chip") && UISkin.Has("circle_frame")) BuildKit();
resultOverlay = MakeImage("ResultOverlay", transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero, new Color(0f, 0f, 0f, 0.65f)).gameObject;
resultTitle = MakeText("Title", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 60f), new Vector2(1000f, 160f), 110, TextAnchor.MiddleCenter);
resultTitle.transform.SetParent(resultOverlay.transform, false);
@ -169,6 +192,110 @@ namespace TKS
resultOverlay.SetActive(false);
}
/// <summary>
/// Fable 제안서 §4-8(#1104): 아래 유닛 판(초상 원 150 + 이름 · 병종 원 · 체력 바 · 공/방 칩 · 슬링/사거리 칩) ·
/// 오른쪽 위 수 칩 = 아군·적 아이콘 + 숫자 · 스킬 버튼 뒤 빛(사용 가능일 때 호흡) · 기본 조작 문장 삭제(상황 안내만 유지).
/// </summary>
void BuildKit()
{
var soft = UISkin.Col("text_soft", new Color(0.78f, 0.8f, 0.84f));
// 유닛 판(기존 UnitPanel 판 대신 · 왼쪽 아래 24 · 판 위 내용)
var old = transform.Find("UnitPanel");
if (old != null) old.gameObject.SetActive(false);
unitText.gameObject.SetActive(false);
var card = MakeImage("UnitCard", transform, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(24f, 92f), new Vector2(720f, 196f), UISkin.Col("hud_panel", new Color(0.12f, 0.15f, 0.21f, 0.85f)));
UISkin.Apply(card, "card");
unitCard = card.rectTransform;
unitBody = OutGame.UiKit.Fill("Body", unitCard);
portraitRing = OutGame.UiKit.Skin(OutGame.UiKit.At(unitBody, "Ring", 18f, 22f, 152f, 152f), "circle_frame", Color.white);
var mask = MakeImage("Mask", portraitRing.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(132f, 132f), Color.white);
mask.sprite = OutGame.UiKit.Circle;
mask.gameObject.AddComponent<Mask>().showMaskGraphic = false;
portraitArt = MakeImage("Art", mask.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -40f), new Vector2(230f, 230f), Color.white);
portraitArt.preserveAspect = true;
typeRing = OutGame.UiKit.Skin(OutGame.UiKit.At(unitBody, "TypeRing", 188f, 20f, 52f, 52f), "circle_frame", Color.white);
typeIcon = OutGame.UiKit.Icon(typeRing.transform, "cls_infantry", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 34f);
nameText = OutGame.UiKit.Title(OutGame.UiKit.Label(unitBody, "", 36, Color.white, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(250f, -18f), new Vector2(300f, 56f)), true);
nameText.horizontalOverflow = HorizontalWrapMode.Overflow;
stateText = OutGame.UiKit.Label(unitBody, "", 24, soft, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-20f, -22f), new Vector2(220f, 48f), false);
var hpBg = OutGame.UiKit.Skin(OutGame.UiKit.At(unitBody, "Hp", 188f, 86f, HpBarW, 30f), "hp_bg", UISkin.Col("hp_bg", Color.white));
hpFill = OutGame.UiKit.At(hpBg.transform, "Fill", 4f, 4f, HpBarW - 8f, 22f);
OutGame.UiKit.Skin(hpFill, "hp_fill", UISkin.Col("hp_hi", Color.green));
hpText = OutGame.UiKit.Title(OutGame.UiKit.Label(unitBody, "", 26, Color.white, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(188f + HpBarW + 14f, -80f), new Vector2(160f, 44f)), true);
hpText.horizontalOverflow = HorizontalWrapMode.Overflow;
atkText = OutGame.UiKit.InfoChip(unitBody, "Atk", 188f, 128f, 150f, 48f, "sword", "", 28);
defText = OutGame.UiKit.InfoChip(unitBody, "Def", 350f, 128f, 150f, 48f, "shield", "", 28);
extraText = OutGame.UiKit.InfoChip(unitBody, "Extra", 512f, 128f, 188f, 48f, null, "", 24);
emptyText = OutGame.UiKit.Label(unitCard, "유닛을 선택하세요", 30, soft, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(680f, 60f), false);
// 수 칩 = 아군 · 적 아이콘 + 숫자(Sen)
var cc = transform.Find("CountChip");
if (cc != null && OutGame.UiKit.Icon(cc, "hud_ally", new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(48f, 0f), 56f) != null)
{
countText.gameObject.SetActive(false);
OutGame.UiKit.Icon(cc, "hud_enemy", new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(200f, 0f), 56f);
allyCount = OutGame.UiKit.Title(OutGame.UiKit.Label(cc, "", 40, Color.white, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(84f, 2f), new Vector2(80f, 60f)), true);
enemyCount = OutGame.UiKit.Title(OutGame.UiKit.Label(cc, "", 40, Color.white, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(236f, 2f), new Vector2(80f, 60f)), true);
}
// 스킬 버튼 뒤 빛(사용 가능 = 호흡)
if (UISkin.Has("glow_circle"))
{
skillGlow = MakeImage("SkillGlow", transform, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0.5f, 0.5f), new Vector2(-24f - 125f, 100f + 125f), new Vector2(380f, 380f), UISkin.Col("skill_glow", new Color(0.85f, 0.25f, 0.18f, 0.4f)));
UISkin.Apply(skillGlow, "glow_circle");
skillGlow.transform.SetSiblingIndex(skillPanel.transform.GetSiblingIndex());
var pulse = skillGlow.gameObject.AddComponent<OutGame.UiPulse>();
pulse.min = 0.45f;
pulse.period = 1.2f;
skillGlow.gameObject.SetActive(false);
}
}
/// <summary>유닛 판 값 갱신(매 프레임 · 초상은 선택이 바뀔 때만).</summary>
void RefreshUnitCard(Unit u)
{
bool on = u != null && u.IsAlive;
if (unitBody.gameObject.activeSelf != on) unitBody.gameObject.SetActive(on);
emptyText.gameObject.SetActive(!on);
if (!on)
{
shownUnit = null;
return;
}
if (shownUnit != u)
{
shownUnit = u;
var sp = OutGame.UiKit.ArtSprite(u.ArtId);
portraitArt.sprite = sp;
portraitArt.enabled = sp != null;
portraitRing.color = u.Faction == Faction.Player ? UISkin.Col("hud_turn_player", Color.blue) : UISkin.Col("hud_turn_enemy", Color.red);
typeRing.color = OutGame.UiKit.TypeColor(u.Type);
var tsp = UISkin.Icon(OutGame.UiKit.TypeIconId(u.Type));
if (typeIcon != null && tsp != null) typeIcon.sprite = tsp;
}
nameText.text = u.DisplayName + (u.IsBoss ? " <size=24>장수</size>" : "");
float r = u.MaxHp > 0 ? Mathf.Clamp01(u.Hp / (float)u.MaxHp) : 0f;
hpFill.sizeDelta = new Vector2(Mathf.Max(12f, (HpBarW - 8f) * r), hpFill.sizeDelta.y);
hpFill.gameObject.SetActive(r > 0f);
hpFill.GetComponent<Image>().color = r > 0.5f ? UISkin.Col("hp_hi", Color.green) : r > 0.25f ? UISkin.Col("hp_mid", Color.yellow) : UISkin.Col("hp_low", Color.red);
hpText.text = u.Hp + "/" + u.MaxHp;
atkText.text = u.Atk.ToString();
defText.text = u.Def.ToString();
string extra = "슬링 " + SkillMods.SlingMax(u).ToString("0.#");
if (UnitTypeInfo.IsRanged(u.Type)) extra = "사거리 " + SkillMods.Range(u).ToString("0.#");
else if (u.Type == UnitType.Spear) extra = "견제 " + SkillMods.OverwatchRadius(u).ToString("0.#");
if (u.Type == UnitType.Strategist) extra = "광역 " + SkillMods.SplashRadius(u, battle.Config).ToString("0.#");
extraText.text = extra;
string state = u.Faction == Faction.Player ? "아군" : "적군";
if (u.Faction == Faction.Player)
{
if (u.BonusMoveAvailable) state = "추가 이동";
else if (u.ActedThisTurn) state = "행동 완료";
}
if (u.Overwatch) state += " · 대기 중";
stateText.text = state;
}
Text MakeText(string name, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 anchoredPos, Vector2 size, int fontSize, TextAnchor align)
{
var go = new GameObject(name);
@ -356,6 +483,11 @@ namespace TKS
? stage.chapter + "-" + stage.stage + " · " + stage.title
: "제" + (battle.StageIndex + 1) + "전투 · " + stage.title + (string.IsNullOrEmpty(stage.subtitle) ? "" : " " + stage.subtitle);
countText.text = "아군 " + battle.AliveCount(Faction.Player) + " · 적 " + battle.AliveCount(Faction.Enemy);
if (allyCount != null)
{
allyCount.text = battle.AliveCount(Faction.Player).ToString();
enemyCount.text = battle.AliveCount(Faction.Enemy).ToString();
}
switch (battle.Phase)
{
@ -382,9 +514,10 @@ namespace TKS
}
else if (battle.Phase == BattlePhase.PlayerTurn)
{
// 기본 조작 문장은 테마 HUD 에서 삭제(Fable §4-8 · 첫 전투 튜토리얼 손으로 대체 예정) · 상황 안내만 유지
hintText.text = battle.OnlyBonusMovesRemain()
? "추가 이동: 유닛을 당겨 이동 · 빈 곳을 탭하면 건너뜀"
: "유닛을 잡고 뒤로 당겼다 놓기 = 이동·공격 · 유닛 탭 = 사거리 · 빈 곳 드래그 = 전장 이동";
: unitCard != null ? "" : "유닛을 잡고 뒤로 당겼다 놓기 = 이동·공격 · 유닛 탭 = 사거리 · 빈 곳 드래그 = 전장 이동";
}
else
{
@ -414,7 +547,14 @@ namespace TKS
{
unitText.text = "유닛을 선택하세요";
}
if (unitCard != null) RefreshUnitCard(selected);
RefreshSkill();
if (skillGlow != null)
{
bool ready = skillPanel.gameObject.activeSelf && battle.Skills != null && selected != null && selected.IsAlive && selected.Faction == Faction.Player
&& battle.Skills.StateOf(selected) == SkillSystem.ActiveState.Ready;
if (skillGlow.gameObject.activeSelf != ready) skillGlow.gameObject.SetActive(ready);
}
bool result = battle.Phase == BattlePhase.StageClear || battle.Phase == BattlePhase.Defeat;
if (resultOverlay.activeSelf != result) resultOverlay.SetActive(result);