보물 · 연무장 · 내정(순찰) · 전투 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 %) color,row_label_strip,,,,,,#FFFFFF0A,편성 라벨 칸 세로 띠(흰 4 %)
font,title,Assets/UI/GUIPro/ResourcesData/Fonts/Sen-ExtraBold.ttf,,,,,,제목·숫자(라틴 Sen · 한글 = 폴백 Malgun Gothic · PD 결정 전) font,title,Assets/UI/GUIPro/ResourcesData/Fonts/Sen-ExtraBold.ttf,,,,,,제목·숫자(라틴 Sen · 한글 = 폴백 Malgun Gothic · PD 결정 전)
color,text_on_yellow,,,,,,#1E1E2A,노랑 Primary 위 글자(Fable §5-8) 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,필드 위 행 판(필드보다 밝게 · 팝업 교환 행) 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_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) 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,,,,상자 카드 광택 skin,card3_light,Frame/CardFrame03_n_Light.png,,1,,,,상자 카드 광택
color,card3_inner,,,,,,#00000040,상자 카드 안쪽(검정 25 %) color,card3_inner,,,,,,#00000040,상자 카드 안쪽(검정 25 %)
color,card3_light,,,,,,#FFFFFF80,상자 카드 광택(흰 50 %) 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 · 장식 비율 유지) 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_bg,Slider/Slider_Basic04_Bg.png,,1.5,,,,가는 진행 바 바탕
skin,bar4_fill,Slider/Slider_Basic04_Fill_Orange.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) 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_crit,Icon_ItemIcons/128/ItemIcon_Damage.Png,,,,,,보물 임시 아이콘 치명
icon,tre_stat_range,Icon_ItemIcons/128/ItemIcon_Gear_Boots.Png,,,,,,보물 임시 아이콘 사거리 icon,tre_stat_range,Icon_ItemIcons/128/ItemIcon_Gear_Boots.Png,,,,,,보물 임시 아이콘 사거리
icon,tre_stat_splash,Icon_ItemIcons/128/ItemIcon_Battle.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 - id: text_on_yellow
color: {r: 0.11764706, g: 0.11764706, b: 0.16470589, a: 1} color: {r: 0.11764706, g: 0.11764706, b: 0.16470589, a: 1}
- id: ribbon_glow - 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 - id: row_on_field
color: {r: 0.24705882, g: 0.25882354, b: 0.38431373, a: 1} color: {r: 0.24705882, g: 0.25882354, b: 0.38431373, a: 1}
- id: banner_pattern - id: banner_pattern
@ -562,9 +562,13 @@ MonoBehaviour:
- id: card3_light - id: card3_light
color: {r: 1, g: 1, b: 1, a: 0.5019608} color: {r: 1, g: 1, b: 1, a: 0.5019608}
- id: chest_glow - 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 - 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: skins:
- id: card - id: card
pack: {fileID: 21300000, guid: 3070a6f23e6ca4273897acf1b7e8c5ce, type: 3} pack: {fileID: 21300000, guid: 3070a6f23e6ca4273897acf1b7e8c5ce, type: 3}
@ -1729,6 +1733,34 @@ MonoBehaviour:
pack: {fileID: 21300000, guid: d031f4b21493c4841914d4257119a4e3, type: 3} pack: {fileID: 21300000, guid: d031f4b21493c4841914d4257119a4e3, type: 3}
samguk: {fileID: 0} samguk: {fileID: 0}
tint: {r: 1, g: 1, b: 1, a: 1} 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 - id: tile_bridge
pack: {fileID: 0} pack: {fileID: 0}
samguk: {fileID: 21300000, guid: 8e5bf7eb1e300474785ff2781eeb5093, type: 3} samguk: {fileID: 21300000, guid: 8e5bf7eb1e300474785ff2781eeb5093, type: 3}

View File

@ -25,6 +25,7 @@ namespace TKS.OutGame
public override void Build() 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); Header("연무장", "탑 · 일일 던전 · 투기장 · 적장 토벌", true);
root = Body(); root = Body();
} }
@ -83,6 +84,11 @@ namespace TKS.OutGame
{ "주간 보스(여포·조조 …) · 누적 피해", "보물 명장 상자", "하루 " + e.RaidDaily, "이번 주 " + boss + " · 누적 " + d.arena.raidDamage.ToString("N0") }, { "주간 보스(여포·조조 …) · 누적 피해", "보물 명장 상자", "하루 " + e.RaidDaily, "이번 주 " + boss + " · 누적 " + d.arena.raidDamage.ToString("N0") },
}; };
float w = 508f, h = 700f; 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++) for (int i = 0; i < 4; i++)
{ {
int k = 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 OpenCount(System.DateTime now)
{ {
int n = 0; int n = 0;

View File

@ -61,12 +61,15 @@ namespace TKS.OutGame
public override void Build() public override void Build()
{ {
Tab = 2; Tab = 2;
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(); var body = Body();
for (int i = 0; i < PageNames.Length; i++) for (int i = 0; i < PageNames.Length; i++)
{ {
int k = 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)); 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++) for (int i = 0; i < tabs.Count; i++)
{ {
bool on = i == page; bool on = i == page;
tabs[i].GetComponent<Image>().color = on ? UiKit.Red : UiKit.Paper; // 선택 = 붉은 옻칠(다른 화면 탭과 같음) if (UISkin.On) UiKit.SetSubTab(tabs[i], on);
UiKit.LabelOf(tabs[i]).color = on ? UiKit.White : UiKit.Ink; 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); pages[i].gameObject.SetActive(on);
} }
UiKit.Clear(pages[page]); UiKit.Clear(pages[page]);
if (page == 0) BuildTraining(pages[0]); 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]); 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) void DoHarvest(bool ad)
{ {
string why; string why;

View File

@ -39,16 +39,32 @@ namespace TKS.OutGame
public override void Build() public override void Build()
{ {
Tab = 1; 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) Header("보물", "", true); // 부제 삭제(설계서 §2-5)
var body = Body(); var body = Body();
for (int i = 0; i < PageNames.Length; i++) for (int i = 0; i < PageNames.Length; i++)
{ {
int k = 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)); 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) public void SetPage(int p)
{ {
page = Mathf.Clamp(p, 0, pages.Count - 1); page = Mathf.Clamp(p, 0, pages.Count - 1);
@ -67,13 +83,18 @@ namespace TKS.OutGame
for (int i = 0; i < tabs.Count; i++) for (int i = 0; i < tabs.Count; i++)
{ {
bool on = i == page; bool on = i == page;
tabs[i].GetComponent<Image>().color = on ? UiKit.Red : UiKit.Paper; // 선택 = 붉은 옻칠(다른 화면 탭과 같음) if (UISkin.On) UiKit.SetSubTab(tabs[i], on);
UiKit.LabelOf(tabs[i]).color = on ? UiKit.White : UiKit.Ink; 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); pages[i].gameObject.SetActive(on);
} }
UiKit.Clear(pages[page]); UiKit.Clear(pages[page]);
if (page == 0) BuildOwned(pages[0]); if (page == 0) BuildOwned(pages[0]);
else if (page == 1) BuildSets(pages[1]); else if (page == 1) BuildSets(pages[1]);
else if (Kit) BuildChestsKit(pages[2]);
else BuildChests(pages[2]); else BuildChests(pages[2]);
} }
@ -95,10 +116,12 @@ namespace TKS.OutGame
cell.sprite = null; // 바닥 = 클릭 판(투명) cell.sprite = null; // 바닥 = 클릭 판(투명)
var t = cell.transform; var t = cell.transform;
UiKit.ItemSlot(t, owned ? "tre_" + it.grade + "_" : "tre_none_", owned); 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)); 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));
if (UiKit.HasFinalIcon("tre_" + it.id)) 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); if (owned) UiKit.DropShadow(ic, 5f);
else ic.color = UiKit.Col("tre_none_icon", UiKit.InkSoft); 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); 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)); // 이름 띠 = 칸 아래에 붙은 카드 띠(CardFrame01_Bottom · Fable §4-5) · 테마가 없으면 둥근 판
var nt = UiKit.FillLabel(band.transform, owned ? it.nameKo : "미보유", 26, owned ? UiKit.White : UiKit.InkSoft); 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; nt.horizontalOverflow = HorizontalWrapMode.Overflow;
} }
@ -128,12 +159,17 @@ namespace TKS.OutGame
int owned = 0; int owned = 0;
foreach (var it in db.Treasures.items) foreach (var it in db.Treasures.items)
if (S.Save.inv.Treasure(it.id) != null) owned++; 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; var grades = db.Economy.TreasureGrades;
for (int i = 0; i < grades.Count; i++) 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)); // 등급 범례 칩 = 서브탭 틀 소형(Round10 · Fable §4-5 「120×56 통일」) · 등급 대표 색
UiKit.FillLabel(chip.transform, TreasureRules.GradeKo(grades[i].Key), 24, UiKit.White); 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; ScrollRect sr;
var grid = UiKit.ScrollGrid("Grid", p, 24f, 24f, 76f, 64f, new Vector2(240f, 290f), new Vector2(16f, 16f), 4, out 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); float v = TreasureRules.StatPct(db, S.Save, s);
if (v > 0f) bonus.Add(TreasureRules.StatKo(s) + " +" + v.ToString("0.#") + "%"); 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, string fx = bonus.Count > 0 ? "전체 효과 " + string.Join(" · ", bonus.ToArray()) : "보물을 모으면 전 유닛에 상시 효과";
new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 8f), new Vector2(1030f, 50f), false); 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) 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) void Step(int dir)
{ {
int n = Mathf.Max(1, S.Db.Chests.chests.Count); int n = Mathf.Max(1, S.Db.Chests.chests.Count);
@ -350,17 +521,10 @@ namespace TKS.OutGame
new OutGameRoot.ModalButton("확인", UiKit.Dark, null)); new OutGameRoot.ModalButton("확인", UiKit.Dark, null));
} }
/// <summary>상자 대표 색 = 테마 chest_&lt;id&gt;(구 코드 상수 → ui_theme.csv · C45 · #1104).</summary>
public static Color ChestColor(string id) public static Color ChestColor(string id)
{ {
switch (id) return UiKit.Col("chest_" + id, UiKit.Slot);
{
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;
} }
} }
} }

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); 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; 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); 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); if (skin == "btn_s_green" && can) UiKit.BtnCost(btn, "ad", "", 36f, 24);
} }
@ -797,7 +797,7 @@ namespace TKS.OutGame
ti.color = UiKit.White; ti.color = UiKit.White;
UiKit.DropShadow(ti, 2f); 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; n.horizontalOverflow = HorizontalWrapMode.Overflow;
} }
y += 140f; y += 140f;

View File

@ -358,7 +358,7 @@ namespace TKS.OutGame
{ {
float h = size + 12f; 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 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; t.horizontalOverflow = HorizontalWrapMode.Overflow;
chip.rectTransform.sizeDelta = new Vector2(Mathf.Max(h + 8f, Mathf.Ceil(t.preferredWidth) + 30f), h); chip.rectTransform.sizeDelta = new Vector2(Mathf.Max(h + 8f, Mathf.Ceil(t.preferredWidth) + 30f), h);
return t; return t;

View File

@ -38,6 +38,27 @@ namespace TKS
Text skillPassive; Text skillPassive;
Text skillLog; 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 minimapBg;
Image minimapView; Image minimapView;
readonly Dictionary<Unit, Image> minimapDots = new Dictionary<Unit, Image>(); 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)); 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)); 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; 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 = 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); resultTitle.transform.SetParent(resultOverlay.transform, false);
@ -169,6 +192,110 @@ namespace TKS
resultOverlay.SetActive(false); 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) Text MakeText(string name, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 anchoredPos, Vector2 size, int fontSize, TextAnchor align)
{ {
var go = new GameObject(name); var go = new GameObject(name);
@ -356,6 +483,11 @@ namespace TKS
? stage.chapter + "-" + stage.stage + " · " + stage.title ? stage.chapter + "-" + stage.stage + " · " + stage.title
: "제" + (battle.StageIndex + 1) + "전투 · " + stage.title + (string.IsNullOrEmpty(stage.subtitle) ? "" : " " + stage.subtitle); : "제" + (battle.StageIndex + 1) + "전투 · " + stage.title + (string.IsNullOrEmpty(stage.subtitle) ? "" : " " + stage.subtitle);
countText.text = "아군 " + battle.AliveCount(Faction.Player) + " · 적 " + battle.AliveCount(Faction.Enemy); 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) switch (battle.Phase)
{ {
@ -382,9 +514,10 @@ namespace TKS
} }
else if (battle.Phase == BattlePhase.PlayerTurn) else if (battle.Phase == BattlePhase.PlayerTurn)
{ {
// 기본 조작 문장은 테마 HUD 에서 삭제(Fable §4-8 · 첫 전투 튜토리얼 손으로 대체 예정) · 상황 안내만 유지
hintText.text = battle.OnlyBonusMovesRemain() hintText.text = battle.OnlyBonusMovesRemain()
? "추가 이동: 유닛을 당겨 이동 · 빈 곳을 탭하면 건너뜀" ? "추가 이동: 유닛을 당겨 이동 · 빈 곳을 탭하면 건너뜀"
: "유닛을 잡고 뒤로 당겼다 놓기 = 이동·공격 · 유닛 탭 = 사거리 · 빈 곳 드래그 = 전장 이동"; : unitCard != null ? "" : "유닛을 잡고 뒤로 당겼다 놓기 = 이동·공격 · 유닛 탭 = 사거리 · 빈 곳 드래그 = 전장 이동";
} }
else else
{ {
@ -414,7 +547,14 @@ namespace TKS
{ {
unitText.text = "유닛을 선택하세요"; unitText.text = "유닛을 선택하세요";
} }
if (unitCard != null) RefreshUnitCard(selected);
RefreshSkill(); 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; bool result = battle.Phase == BattlePhase.StageClear || battle.Phase == BattlePhase.Defeat;
if (resultOverlay.activeSelf != result) resultOverlay.SetActive(result); if (resultOverlay.activeSelf != result) resultOverlay.SetActive(result);