UI 상업용 재설계 ① 영웅 상세 + 공통 크롬(#1104) — 확정 목업 hero_detail.html 좌표·부품·색(본문 y = 목업 y − 224)

- 영웅 상세: 등급 색 바탕(글로우 + 문양 타일 · 화면 전체) · 전투력 칩 · 일러(빛 원 · 그림자 · 좌우 밀기 80 px = 영웅 전환) · 장비 6칸(ItemFrame 5층 · 장비 등급 색 · 장착 체크 / 가방 초록 화살표 / 없음 흐림) · 레벨 행(배지 · 바 · 레벨업 녹 + 골드 · 승급 하늘 장비 n/6) · 이름 띠(세력 · 병종 원 · 등급 칩 · ★) · 스킬 패널(액티브 + 쿨 칩 · Lv 칩 · 행마다 레벨업 = 스킬서 · 골드 · 상한 = 회색 최대) · 메뉴 4버튼(정보 = 상세정보 + 도감 · 진화 = 조각 팝업 + 알림 점 · 각성 · 외형 = 준비 중) · 헤더 오른쪽 보유 n / 전체 칩 · 1080 폭 틀 가운데 고정
- 공통 크롬: 재화 바 띠 없앰(칩 · 아이콘 84 밖으로 · 재화별 + 단추) · 헤더 PanelFrame03 + 왼쪽 붙은 뒤로 · 하단 탭 TabFocus 솟음 판 + 광택 · 아이콘 104/118 · 라벨 = 선택 탭만
- UiKit.TypeKey · 캡처 드라이버 = 영웅 상세 등급 변형(그 영웅 장비 세트 등급) · 테스트 HeroDetail_Redesign_SkillRow_Swipe_Menu(117/117)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-09 11:11:20 +09:00
parent 8d58487517
commit 14bec33f2b
5 changed files with 587 additions and 239 deletions

View File

@ -92,39 +92,70 @@ namespace TKS.OutGame
}
/// <summary>
/// 영웅 상세(img06 · 기획 S6 v4) — 전투력 · 일러 + 양옆 장비 6칸(빈 칸 탭 = 가방에서 장착 · 1칸 = 1강) · 이름 띠(병종 · 이름 · ★) ·
/// 스킬 정보(액티브·패시브 = 이름·효과·Lv·쿨 · 표시만 · 스킬 레벨업 = B 단계) · Lv/경험 바 + 레벨업(골드) · 승급(6칸) · 조각 바 + 진화 · 상세정보 · 도감 · ◀ ▶ 전환.
/// 영웅 상세(UI 상업용 재설계 ① · PD 2026-11-09 확정 목업 hero_detail.html = 좌표·부품·색 SOT · 본문 y = 목업 y − 224) —
/// 등급 색 바탕(글로우 + 문양 · 화면 전체) · 전투력 칩 · 일러(좌우 밀기 = 영웅 전환) + 양옆 장비 6칸 · 레벨 행(레벨업 · 승급) · 이름 띠(세력 · 병종 · 등급 · ★) ·
/// 스킬 패널(액티브 · 패시브 · 행마다 레벨업) · 메뉴 4버튼(정보 = 상세정보 + 도감 · 진화 = 조각 팝업 · 각성 · 외형 = 준비 중). 색·스프라이트 = 테마 표(ui_theme.csv) id.
/// </summary>
public class HeroDetailScreen : OutGameScreen
{
const float SwipeMin = 80f; // 일러 밀기 = 가로 80 px 이상(기준 해상도 px)
static readonly string[] MenuNames = { "정보", "진화", "각성", "외형" };
static readonly string[] MenuIcons = { "info", "star_l", "fire", "top" };
static readonly Vector2[] MenuIconSize = { new Vector2(26f, 60f), new Vector2(58f, 62f), new Vector2(64f, 50f), new Vector2(62f, 46f) };
string id;
readonly List<string> order = new List<string>();
Image bgFill;
Image bgGlow;
Text ownedText;
Text power;
RectTransform art;
readonly Image[] slotImg = new Image[6];
readonly Text[] slotName = new Text[6];
readonly Text[] slotState = new Text[6];
float dragX;
readonly GameObject[] slotRoot = new GameObject[6];
readonly Image[] slotGlow = new Image[6];
readonly Image[] slotBg = new Image[6];
readonly Image[] slotDeco = new Image[6];
readonly Image[] slotLight = new Image[6];
readonly Image[] slotIcon = new Image[6];
readonly Image[] slotMark = new Image[6];
RectTransform starRow;
Image typeDot;
Text typeMark;
Text nameText;
Text subText;
Text starText;
Text[] skillName = new Text[2];
Text[] skillDesc = new Text[2];
Text[] skillChip = new Text[2];
readonly Text[] slotName = new Text[6];
readonly Image[] slotCheck = new Image[6];
readonly Image[] slotBag = new Image[6];
Text lvBadge;
Text lvText;
Text expText;
RectTransform expBar;
Button levelBtn;
Button promoteBtn;
Text shardText;
RectTransform shardBar;
Button evolveBtn;
Button skillBtn;
RectTransform expFill;
Image levelImg;
Text levelLabel;
Text levelCost;
Image goldIcon;
Image promoteImg;
Text promoteSub;
Image bandImg;
Image flagIcon;
Image clsIcon;
Image paraImg;
Text gradeText;
Text nameText;
Text typeText;
RectTransform starRow;
Image panelImg;
GameObject coolChip;
Text coolText;
readonly Image[] rowImg = new Image[2];
readonly Image[] skFrame = new Image[2];
readonly Image[] skCls = new Image[2];
readonly Text[] skName = new Text[2];
readonly Text[] skHanja = new Text[2];
readonly Text[] skDesc = new Text[2];
readonly Text[] skLv = new Text[2];
readonly Image[] skBtn = new Image[2];
readonly Text[] skBtnText = new Text[2];
readonly Text[] skBtnSub = new Text[2];
readonly GameObject[] skCost = new GameObject[2];
readonly Text[] skBooks = new Text[2];
readonly Text[] skGold = new Text[2];
Image evolveDot;
public string HeroId
{
@ -140,107 +171,257 @@ namespace TKS.OutGame
foreach (var o in S.Owned(h.isSoldier)) order.Add(o.id);
}
// ---------- 목업 좌표 도우미(부모 왼쪽 위 기준 · y 아래로) ----------
static Color Col(string colorId)
{
return UISkin.Col(colorId, Color.white);
}
static RectTransform At(Transform p, string name, float x, float y, float w, float h)
{
return UiKit.Rect(name, p, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(x, -y), new Vector2(w, h));
}
static Image Spr(Transform p, string name, string skin, float x, float y, float w, float h, string color, bool raycast = false)
{
return UiKit.Skin(At(p, name, x, y, w, h), skin, Col(color), raycast);
}
Image Press(Transform p, string name, string skin, float x, float y, float w, float h, string color, System.Action onClick)
{
var img = Spr(p, name, skin, x, y, w, h, color, true);
img.gameObject.AddComponent<Button>().onClick.AddListener(() => onClick());
return img;
}
static Image Ico(Transform p, string icon, float x, float y, float w, float h, float shadow = 0f)
{
var img = At(p, "Icon", x, y, w, h).gameObject.AddComponent<Image>();
img.preserveAspect = true;
img.raycastTarget = false;
SetIcon(img, icon, 1f);
if (shadow > 0f) Drop(img, shadow);
return img;
}
/// <summary>테마 아이콘(팩 임시 = 표 tint · 신규 제작 = 원색) · alpha = 곱.</summary>
static void SetIcon(Image img, string icon, float alpha)
{
var e = UISkin.Theme != null && icon != null ? UISkin.Theme.IconEntryOf(icon) : null;
img.sprite = e != null ? e.Sprite : null;
var c = e != null && e.samguk == null ? e.tint : Color.white;
c.a *= alpha;
img.color = c;
img.enabled = img.sprite != null;
}
static void Drop(Graphic g, float d)
{
var s = g.gameObject.AddComponent<Shadow>();
s.effectColor = Col("icon_shadow");
s.effectDistance = new Vector2(0f, -d);
}
/// <summary>목업 text() — y = 글자 윗변 · title = Sen(+ 한글 폴백) · false = 맑은 고딕 · stroke &lt; 0 = 기본(크기 × 0.06) · 0 = 없음 · 그 밖 = 목업 px(절반).</summary>
static Text Txt(Transform p, string s, float x, float y, int size, string color, TextAnchor align = TextAnchor.MiddleLeft, float w = 0f, bool title = true, float stroke = -1f, bool shadow = true)
{
float bw = w > 0f ? w : 900f;
float px = align == TextAnchor.MiddleCenter ? 0.5f : align == TextAnchor.MiddleRight ? 1f : 0f;
var rt = UiKit.Rect("Text", p, new Vector2(0f, 1f), new Vector2(px, 0.5f), new Vector2(x + bw * px, -(y + size * 0.5f)), new Vector2(bw, size * 1.6f));
var t = rt.gameObject.AddComponent<Text>();
t.font = title ? UISkin.TitleFont : TextUtil.Font;
t.fontSize = size;
t.fontStyle = FontStyle.Bold;
t.alignment = align;
t.color = Col(color);
t.horizontalOverflow = HorizontalWrapMode.Overflow;
t.verticalOverflow = VerticalWrapMode.Overflow;
t.supportRichText = true;
t.raycastTarget = false;
t.text = s;
if (stroke != 0f) UISkin.Stroke(t, Col("stroke"), stroke < 0f ? Mathf.Max(2f, size * 0.06f) : stroke * 0.5f);
if (shadow)
{
var sh = t.gameObject.AddComponent<Shadow>();
sh.effectColor = Col("text_shadow");
sh.effectDistance = new Vector2(0f, -Mathf.Round(size * 0.07f));
}
return t;
}
static Text Desc(Transform p, float x, float y, float w)
{
var t = Txt(p, "", x, y, 26, "text_desc", TextAnchor.UpperLeft, w, false, 0f, false);
var rt = t.rectTransform;
rt.pivot = new Vector2(0f, 1f);
rt.anchoredPosition = new Vector2(x, -y);
rt.sizeDelta = new Vector2(w, 70f);
t.horizontalOverflow = HorizontalWrapMode.Wrap;
t.lineSpacing = 1.05f;
return t;
}
static void PlaceX(Graphic g, float x)
{
var rt = g.rectTransform;
rt.anchoredPosition = new Vector2(x, rt.anchoredPosition.y);
}
// ---------- 화면 ----------
public override void Build()
{
Tab = 0;
Header("영웅", "레벨 · 승급 · 진화 · 스킬", true);
var body = Body();
// ① 바탕(화면 전체 · 재화 바·헤더 뒤까지) = 등급 색 + 세로 글로우 + 문양 타일
bgFill = UiKit.Fill("GradeBg", Rt).gameObject.AddComponent<Image>();
bgFill.raycastTarget = false;
bgGlow = UiKit.Skin(UiKit.Fill("GradeGlow", Rt), "bg_glow", Color.white);
bgGlow.enabled = UISkin.Has("bg_glow");
var pat = UiKit.Skin(UiKit.Fill("GradePattern", Rt), "bg_pattern", Col("grade_pattern"));
pat.type = Image.Type.Tiled;
pat.enabled = UISkin.Has("bg_pattern");
// ① 카드: 전투력 · 일러 · 장비 6칸 · 이름 띠
var card = UiKit.TopBand("Card", body, 16f, 720f, 24f, 24f);
var cimg = UiKit.Paint(card, UiKit.Paper);
UiKit.Border(cimg, UiKit.Line, 3f);
UiKit.Dot("Halo", card, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -100f), 500f, new Color(0.93f, 0.9f, 0.84f));
power = UiKit.Label(card, "", 44, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -16f), new Vector2(560f, 70f));
UiKit.Btn("Prev", card, "◀", 40, UiKit.Dark, UiKit.White, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(20f, -16f), new Vector2(96f, 72f), () => Step(-1));
UiKit.Btn("Next", card, "▶", 40, UiKit.Dark, UiKit.White, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-20f, -16f), new Vector2(96f, 72f), () => Step(1));
art = UiKit.Rect("Art", card, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -96f), new Vector2(560f, 500f));
var head = Header("영웅", "", true);
var own = UiKit.Rect("Owned", head, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-24f, 0f), new Vector2(244f, 62f));
UiKit.Skin(own, "chip", Col("chip_dark"));
ownedText = Txt(own, "", 0f, 15f, 32, "white", TextAnchor.MiddleCenter, 244f);
// 본문 = 목업 1080 폭 틀(화면이 낮아 본문이 줄면서 가로로 늘어나도 가운데 고정)
var body = UiKit.Rect("Mock", Body(), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), Vector2.zero, new Vector2(OutGameRoot.Reference.x, OutGameRoot.BodyH));
// ② 일러 = 빛 원 + 발밑 그림자 + 그림(좌우 밀기 = 이전/다음 영웅)
Spr(body, "ArtGlow", "glow_circle", 210f, 14f, 660f, 660f, "hero_glow");
Spr(body, "ArtShadow", "oval_shadow", 310f, 550f, 460f, 64f, "hero_shadow");
art = At(body, "Art", 260f, 94f, 560f, 500f);
art.gameObject.AddComponent<Image>().color = Color.clear;
var drag = art.gameObject.AddComponent<TapDrag>();
drag.OnDragStart = e => dragX = 0f;
drag.OnDragMove = e => dragX += e.delta.x;
drag.OnDragEnd = e => Swipe(dragX / ScaleOf(art));
// 전투력 칩(두 장비 열 사이 위)
var pw = Spr(body, "Power", "chip", 380f, 24f, 320f, 70f, "chip_dark").transform;
Spr(pw, "Line", "chip_line", 6f, 6f, 308f, 58f, "chip_line");
Ico(pw, "sword", 18f, 4f, 62f, 62f);
Txt(pw, "전투력", 88f, 23f, 28, "text_soft", TextAnchor.MiddleLeft, 0f, true, 0f, false);
power = Txt(pw, "", 180f, 11f, 50, "white");
// ◀ ▶ = 장비 칸 바깥(장비 열 세로 가운데)
Arrow(body, "Prev", "arrow_prev", 10f, -1);
Arrow(body, "Next", "arrow_next", 1026f, 1);
// 장비 6칸(182×178 · ItemFrame 5층 · 장비 등급 색) — 탭 = 장착 · 필요 장비
for (int i = 0; i < 6; i++)
{
int k = i;
bool left = i < 3;
int row = i % 3;
var a = left ? new Vector2(0f, 1f) : new Vector2(1f, 1f);
var b = UiKit.Btn("Slot" + i, card, "", 24, UiKit.Slot, UiKit.Ink, a, a, new Vector2(left ? 22f : -22f, -104f - row * 172f), new Vector2(196f, 156f), () => OnSlot(k));
slotImg[i] = b.GetComponent<Image>();
UiKit.LabelOf(b).text = "";
if (UISkin.On)
{
// 팩 등급 칸(ItemFrame) + 장비 아이콘(테마 gear_<kind> · 임시) + 이름 한 줄 + 상태
slotIcon[i] = UiKit.Rect("GearIcon", b.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 8f), new Vector2(72f, 72f)).gameObject.AddComponent<Image>();
slotIcon[i].preserveAspect = true;
slotIcon[i].raycastTarget = false;
slotMark[i] = UiKit.Rect("Mark", b.transform, new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), new Vector2(-14f, -14f), new Vector2(44f, 44f)).gameObject.AddComponent<Image>();
slotMark[i].preserveAspect = true;
slotMark[i].raycastTarget = false;
slotName[i] = UiKit.Label(b.transform, "", 28, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -6f), new Vector2(186f, 40f));
slotState[i] = UiKit.Label(b.transform, "", 22, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 10f), new Vector2(186f, 34f));
UISkin.Stroke(slotState[i], UiKit.White, 1.5f);
var s = At(body, "Slot" + i, i < 3 ? 66f : 832f, 24f + (i % 3) * 206f, 182f, 178f);
slotRoot[i] = s.gameObject;
slotGlow[i] = Spr(s, "Glow", "item_glow", -8f, -8f, 198f, 194f, "white");
slotBg[i] = Spr(s, "Bg", "item_bg", 0f, 0f, 182f, 178f, "white", true);
slotDeco[i] = Spr(s, "Deco", "item_deco", 4f, 4f, 174f, 170f, "white");
slotLight[i] = Spr(s, "Light", "item_light", 8f, 6f, 166f, 82f, "white");
Spr(s, "Border", "item_border", 0f, 0f, 182f, 178f, "gear_border");
slotIcon[i] = Ico(s, null, 41f, 17f, 100f, 100f, 5f);
Spr(s, "Plate", "plate12", 10f, 130f, 162f, 38f, "gear_plate");
slotName[i] = Txt(s, "", 10f, 135f, 26, "white", TextAnchor.MiddleCenter, 162f, false, 0f, false);
slotCheck[i] = Ico(s, "check", 138f, -12f, 54f, 52f, 3f);
slotBag[i] = Ico(s, "up_green", 142f, -14f, 48f, 56f, 3f);
s.gameObject.AddComponent<Button>().onClick.AddListener(() => OnSlot(k));
}
else
{
slotName[i] = UiKit.Label(b.transform, "", 30, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -12f), new Vector2(186f, 84f));
slotState[i] = UiKit.Label(b.transform, "", 24, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 10f), new Vector2(186f, 44f), false);
}
}
var band = UiKit.Rect("NameBand", card, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), Vector2.zero, new Vector2(0f, 116f));
band.anchorMin = new Vector2(0f, 0f);
band.anchorMax = new Vector2(1f, 0f);
band.offsetMin = new Vector2(0f, 0f);
band.offsetMax = new Vector2(0f, 116f);
UiKit.Paint(band, UiKit.Dark);
typeDot = UiKit.Dot("TypeDot", band, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(24f, 0f), 80f, UiKit.Red);
typeMark = UiKit.FillLabel(typeDot.transform, "", 40, UiKit.White);
nameText = UiKit.Label(band, "", 60, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(124f, 4f), new Vector2(300f, 90f));
nameText.horizontalOverflow = HorizontalWrapMode.Overflow;
subText = UiKit.Label(band, "", 28, new Color(0.85f, 0.8f, 0.75f), TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(420f, 0f), new Vector2(330f, 80f), false);
starText = UiKit.Label(band, "", 52, UiKit.Star, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-24f, 0f), new Vector2(300f, 80f));
if (UISkin.Icon("star_on") != null) starRow = UiKit.Rect("StarRow", band, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-20f, 0f), new Vector2(300f, 60f));
UiKit.Title(nameText, true);
// ② 스킬 정보
var skill = UiKit.TopBand("Skill", body, 752f, 400f, 24f, 24f);
var simg = UiKit.Paint(skill, UiKit.Paper);
UiKit.Border(simg, UiKit.Line, 3f);
UiKit.Label(skill, "스킬", 40, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(28f, -14f), new Vector2(300f, 60f));
UiKit.Label(skill, "액티브 1 · 패시브 1", 26, UiKit.InkSoft, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-28f, -14f), new Vector2(400f, 60f), false);
// ③ 레벨 행 = 배지 + 레벨 바 · 레벨업(녹 Primary + 골드) · 승급(하늘)
Ico(body, "lv_badge", 30f, 650f, 92f, 96f, 3f);
lvBadge = Txt(body, "", 30f, 674f, 46, "white", TextAnchor.MiddleCenter, 92f);
lvText = Txt(body, "", 132f, 648f, 34, "white");
expText = Txt(body, "", 400f, 654f, 28, "text_desc", TextAnchor.MiddleRight, 190f, false, 3f);
Spr(body, "LvBar", "lv_bg", 116f, 694f, 478f, 54f, "white");
expFill = (RectTransform)Spr(body, "LvFill", "lv_fill", 126f, 700f, 458f, 38f, "lv_fill").transform;
levelImg = Press(body, "LevelUp", "btn_l_green", 612f, 640f, 208f, 112f, "white", OnLevelUp);
levelLabel = Txt(levelImg.transform, "레벨업", 0f, 16f, 38, "text_on_green", TextAnchor.MiddleCenter, 208f, false, 0f, false);
goldIcon = Ico(levelImg.transform, "cur_gold", 48f, 56f, 40f, 40f);
levelCost = Txt(levelImg.transform, "", 92f, 60f, 32, "text_on_green", TextAnchor.MiddleLeft, 0f, true, 0f, false);
promoteImg = Press(body, "Promote", "btn_sky", 834f, 640f, 222f, 112f, "white", OnPromote);
Txt(promoteImg.transform, "승급", 0f, 14f, 38, "white", TextAnchor.MiddleCenter, 222f, false);
promoteSub = Txt(promoteImg.transform, "", 0f, 60f, 28, "white", TextAnchor.MiddleCenter, 222f, false, 3f);
// ④ 이름 띠 = 어두운 띠 + 세력 깃발 · 병종 원 · 등급 칩 · 이름 · 병종 · ★
bandImg = Spr(body, "NameBand", "card", 24f, 776f, 1032f, 114f, "white");
Spr(body, "BandLight", "band_light", 40f, 782f, 1000f, 12f, "band_light");
flagIcon = Ico(body, null, 44f, 789f, 88f, 88f, 3f);
Spr(body, "TypeRing", "circle_frame", 140f, 794f, 78f, 78f, "white");
Spr(body, "TypeIn", "circle_frame", 144f, 798f, 70f, 70f, "circle_inner");
clsIcon = Ico(body, null, 147f, 801f, 64f, 64f);
paraImg = Spr(body, "Grade", "para_green", 232f, 805f, 96f, 56f, "white");
gradeText = Txt(body, "", 232f, 811f, 38, "white", TextAnchor.MiddleCenter, 96f);
nameText = Txt(body, "", 344f, 797f, 66, "white");
typeText = Txt(body, "", 500f, 819f, 30, "text_type", TextAnchor.MiddleLeft, 0f, false, 0f, false);
starRow = At(body, "Stars", 742f, 803f, 290f, 60f);
// ⑤ 스킬 패널 = 제목(액티브 + 쿨 칩 · 맨 오른쪽 Lv 칩 / 패시브 + Lv 칩) · 행 = 틀 + 병종 글리프 · 이름 · 한자 · 설명 · 레벨업
panelImg = Spr(body, "SkillPanel", "panel12", 24f, 920f, 1032f, 444f, "white");
for (int i = 0; i < 2; i++)
{
var c = UiKit.TopBand("Skill" + i, skill, 86f + i * 152f, 140f, 20f, 20f);
var ci = UiKit.Paint(c, new Color(0.98f, 0.96f, 0.9f));
UiKit.Border(ci, UiKit.Line, 2f);
var d = UiKit.Dot("Kind", c, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(18f, 0f), 92f, i == 0 ? UiKit.Red : UiKit.Dark);
UiKit.FillLabel(d.transform, i == 0 ? "액" : "패", 40, UiKit.White);
skillName[i] = UiKit.Label(c, "", 34, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(128f, -8f), new Vector2(560f, 56f));
skillName[i].horizontalOverflow = HorizontalWrapMode.Overflow;
skillDesc[i] = UiKit.Label(c, "", 26, UiKit.InkSoft, TextAnchor.UpperLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(128f, -64f), new Vector2(840f, 72f), false);
var chip = UiKit.Box("Chip", c, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-14f, -12f), new Vector2(240f, 46f), i == 0 ? UiKit.Red : UiKit.Dark);
skillChip[i] = UiKit.FillLabel(chip.transform, "", 24, UiKit.White);
int k = i;
float ty = 934f + i * 214f, y = ty + 54f;
Txt(body, i == 0 ? "액티브" : "패시브", 52f, ty + 4f, 36, "white");
if (i == 0)
{
var cc = Spr(body, "Cool", "chip", 184f, ty, 112f, 46f, "chip_cool");
coolChip = cc.gameObject;
coolText = Txt(cc.transform, "", 0f, 9f, 26, "white", TextAnchor.MiddleCenter, 112f, false, 3f);
}
var lc = Spr(body, "SkillLv" + i, "chip", 912f, ty, 112f, 46f, "chip_dark");
skLv[i] = Txt(lc.transform, "", 0f, 9f, 26, "white", TextAnchor.MiddleCenter, 112f, true, 3f);
rowImg[i] = Spr(body, "SkillRow" + i, "panel12", 44f, y, 992f, 146f, "white");
skFrame[i] = Ico(body, null, 58f, y + 10f, 126f, 126f, 4f);
skCls[i] = Ico(body, null, 80f, y + 32f, 82f, 82f, 3f);
skName[i] = Txt(body, "", 200f, y + 18f, 42, "white");
skHanja[i] = Txt(body, "", 200f, y + 30f, 28, "white", TextAnchor.MiddleLeft, 0f, false, 0f, false);
skDesc[i] = Desc(body, 200f, y + 74f, 600f);
skBtn[i] = Press(body, "SkillUp" + i, "btn_purple", 818f, y + 18f, 204f, 110f, "white", () => OnSkillRow(k));
var bt = skBtn[i].transform;
skBtnText[i] = Txt(bt, "", 0f, 12f, 34, "white", TextAnchor.MiddleCenter, 204f, false);
skBtnSub[i] = Txt(bt, "", 0f, 58f, 24, "text_max_sub", TextAnchor.MiddleCenter, 204f, false, 3f);
var cost = At(bt, "Cost", 0f, 0f, 204f, 110f);
skCost[i] = cost.gameObject;
Ico(cost, "cur_skillbook", 22f, 56f, 38f, 38f);
skBooks[i] = Txt(cost, "", 62f, 60f, 28, "white", TextAnchor.MiddleLeft, 0f, true, 3f);
Ico(cost, "cur_gold", 98f, 59f, 32f, 32f);
skGold[i] = Txt(cost, "", 132f, 63f, 22, "text_book", TextAnchor.MiddleLeft, 0f, true, 3f);
}
// ③ 레벨 · 승급
var lvRow = UiKit.TopBand("Level", body, 1168f, 110f, 24f, 24f);
lvText = UiKit.Label(lvRow, "", 42, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(4f, 0f), new Vector2(330f, 60f));
lvText.horizontalOverflow = HorizontalWrapMode.Overflow;
expText = UiKit.Label(lvRow, "", 28, UiKit.Ink, TextAnchor.MiddleRight, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(330f, 0f), new Vector2(230f, 60f));
expBar = UiKit.Bar("ExpBar", lvRow, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(4f, 10f), new Vector2(556f, 30f), new Color(0.86f, 0.82f, 0.75f), new Color(0.47f, 0.38f, 0.32f));
levelBtn = UiKit.Btn("LevelUp", lvRow, "", 30, UiKit.Red, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-252f, 0f), new Vector2(204f, 106f), OnLevelUp);
promoteBtn = UiKit.Btn("Promote", lvRow, "", 30, UiKit.Disabled, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(0f, 0f), new Vector2(236f, 106f), OnPromote);
// ⑥ 메뉴 4버튼 = 정보 · 진화 · 각성 · 외형(진화 가능 = 알림 점)
for (int i = 0; i < 4; i++)
{
int k = i;
var b = Press(body, "Menu" + i, "round08", 34f + i * 258f, 1390f, 236f, 108f, "menu_btn", () => OnMenu(k));
Spr(b.transform, "Light", "band_light", 14f, 8f, 208f, 12f, "menu_light");
var sz = MenuIconSize[i];
Ico(b.transform, MenuIcons[i], 30f + (64f - sz.x) / 2f, 54f - sz.y / 2f, sz.x, sz.y, 3f);
Txt(b.transform, MenuNames[i], 104f, 34f, 40, "white", TextAnchor.MiddleLeft, 0f, false);
}
evolveDot = Ico(body, "new_dot", 498f, 1380f, 36f, 36f);
}
// ④ 조각 · 진화
var shRow = UiKit.TopBand("Shard", body, 1290f, 96f, 24f, 24f);
UiKit.IconOrDot(shRow, "cur_shard", "조", UiKit.Purple, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(0f, -14f), 58f);
UiKit.Label(shRow, "영웅 조각", 32, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(64f, 0f), new Vector2(300f, 46f));
shardBar = UiKit.Bar("ShardBar", shRow, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(64f, 4f), new Vector2(690f, 42f), new Color(0.86f, 0.82f, 0.75f), UiKit.Purple);
shardText = UiKit.Label(shardBar.parent, "", 28, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(680f, 42f));
evolveBtn = UiKit.Btn("Evolve", shRow, "", 30, UiKit.Red, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(0f, 0f), new Vector2(236f, 96f), OnEvolve);
void Arrow(Transform body, string name, string icon, float x, int d)
{
var hit = At(body, name, x - 18f, 249f, 80f, 140f);
hit.gameObject.AddComponent<Image>().color = Color.clear;
hit.gameObject.AddComponent<Button>().onClick.AddListener(() => Step(d));
Ico(hit, icon, 18f, 27f, 44f, 86f);
}
// ⑤ 상세정보 · 도감 · 스킬 레벨업(B)
var btnRow = UiKit.TopBand("Buttons", body, 1402f, 96f, 24f, 24f);
float bw = (1032f - 40f) / 3f;
UiKit.Btn("Detail", btnRow, "상세정보", 36, UiKit.Dark, UiKit.White, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), Vector2.zero, new Vector2(bw, 96f), ShowStats);
UiKit.Btn("Codex", btnRow, "도감", 36, UiKit.Dark, UiKit.White, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(bw, 96f), () => ShowCodex(Root));
skillBtn = UiKit.Btn("SkillUp", btnRow, "스킬 레벨업", 32, UiKit.Red, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(bw, 96f), OnSkillUp);
static float ScaleOf(Transform t)
{
var cv = t.GetComponentInParent<Canvas>();
return cv != null && cv.scaleFactor > 0f ? cv.scaleFactor : 1f;
}
/// <summary>일러 밀기(기준 px · 왼쪽 = 다음 · 오른쪽 = 이전 · 80 px 미만 = 무시).</summary>
public void Swipe(float dx)
{
if (Mathf.Abs(dx) >= SwipeMin) Step(dx < 0f ? 1 : -1);
}
void Step(int d)
@ -259,122 +440,176 @@ namespace TKS.OutGame
var lv = S.Save.Level(id);
var hs = S.Save.Hero(id);
var st = S.StatsOf(id);
power.text = "전투력 " + st.power.ToString("N0");
string g = h.isSoldier || hs == null || string.IsNullOrEmpty(hs.grade) ? "white" : hs.grade;
string G = "grade_" + g + "_";
bgFill.color = Col(G + "bg");
bgGlow.color = Col(G + "glow");
bandImg.color = Col(G + "band");
panelImg.color = Col(G + "stat");
for (int i = 0; i < 2; i++) rowImg[i].color = Col(G + "row");
if (UISkin.Apply(paraImg, "para_" + g)) paraImg.color = Col(G + "chip");
paraImg.gameObject.SetActive(!h.isSoldier);
gradeText.gameObject.SetActive(!h.isSoldier);
gradeText.text = UiKit.GradeKo(g);
int total = 0;
foreach (var o in db.Heroes.heroes)
if (o.isSoldier == h.isSoldier) total++;
ownedText.text = "보유 " + S.Owned(h.isSoldier).Count + " / " + total;
power.text = st.power.ToString("N0");
UiKit.Clear(art);
UiKit.Portrait("Hero", art, h, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(560f, 500f), false, 1f, new Color(1f, 1f, 1f, 0f));
UiKit.Portrait("Hero", art, h, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(560f, 500f), false, 1f, Color.clear);
// 장비: 장착 = 체크 · 가방 보유 = 초록 화살표 · 없음 = 흐림
for (int i = 0; i < 6; i++)
{
string need = h.isSoldier ? null : HeroRules.RequiredGear(db, h, hs, i);
var g = db.Gear.Get(need);
bool on = hs != null && i < hs.gear.Count && !string.IsNullOrEmpty(hs.gear[i]);
int have = need != null ? S.Save.inv.GearCount(need) : 0;
slotImg[i].gameObject.SetActive(need != null);
slotRoot[i].SetActive(need != null);
if (need == null) continue;
slotName[i].text = g != null ? g.nameKo : need;
if (slotIcon[i] != null)
{
// 등급 칸 + 아이콘 · 장착 = 체크 · 가방 보유 = 알림 점 · 없음 = 아이콘 흐리게(글자 = 흰 + 외곽선)
UiKit.GradeFrame(slotImg[i], g != null ? g.grade : "white");
var gs = UISkin.Icon(g != null ? "gear_" + g.kind : null);
var ge = gs != null ? UISkin.Theme.IconEntryOf("gear_" + g.kind) : null;
slotIcon[i].sprite = gs;
slotIcon[i].enabled = gs != null;
var tint = ge != null && ge.samguk == null ? ge.tint : Color.white;
slotIcon[i].color = new Color(tint.r, tint.g, tint.b, on || have > 0 ? 1f : 0.4f);
slotMark[i].sprite = UISkin.Icon(on ? "check" : "new_dot");
slotMark[i].enabled = (on || have > 0) && slotMark[i].sprite != null;
slotMark[i].color = on ? Color.white : UISkin.Theme.IconEntryOf("new_dot") != null ? UISkin.Theme.IconEntryOf("new_dot").tint : UiKit.Red;
string gk = UiKit.GradeKo(g != null ? g.grade : "");
slotState[i].color = on ? UiKit.Ok : have > 0 ? UiKit.Red : UiKit.InkSoft;
slotState[i].text = on ? "장착 · " + gk : have > 0 ? "탭하여 장착 ×" + have : "없음 · " + gk;
continue;
}
if (on)
{
slotImg[i].color = new Color(0.9f, 0.95f, 0.86f);
slotState[i].text = "<color=#3a6a30>○ 장착</color> · " + UiKit.GradeKo(g != null ? g.grade : "");
}
else if (have > 0)
{
slotImg[i].color = new Color(1f, 0.93f, 0.86f);
slotState[i].text = "<color=#a03a22>탭하여 장착 ×" + have + "</color>";
}
else
{
slotImg[i].color = UiKit.Slot;
slotState[i].text = "없음 · " + UiKit.GradeKo(g != null ? g.grade : "");
}
var gd = db.Gear.Get(need);
bool on = hs != null && i < hs.gear.Count && !string.IsNullOrEmpty(hs.gear[i]);
bool bag = !on && S.Save.inv.GearCount(need) > 0;
bool lit = on || bag;
string gg = "gear_" + (gd != null && !string.IsNullOrEmpty(gd.grade) ? gd.grade : "white") + "_";
slotGlow[i].color = lit ? Col(gg + "glow") : Color.clear;
slotBg[i].color = Col(gg + "bg");
slotDeco[i].color = Col(gg + "deco");
slotLight[i].color = Col(gg + "light");
SetIcon(slotIcon[i], gd != null ? "gear_" + gd.kind : null, lit ? 1f : Col("gear_off_icon").a);
slotIcon[i].GetComponent<Shadow>().enabled = lit;
slotName[i].text = gd != null ? gd.nameKo : need;
slotName[i].color = Col(lit ? "white" : "text_off");
slotCheck[i].gameObject.SetActive(on && slotCheck[i].sprite != null);
slotBag[i].gameObject.SetActive(bag && slotBag[i].sprite != null);
}
typeDot.color = UiKit.TypeColor(h.type);
typeMark.text = UnitTypeInfo.Mark(h.type);
nameText.text = h.nameKo;
subText.text = UnitTypeInfo.Korean(h.type) + " · " + (h.isSoldier ? "병사 유닛" : UiKit.GradeKo(hs.grade) + " 등급");
starText.text = h.isSoldier ? UiKit.Stars(h.rarity, 3) : UiKit.Stars(hs.star, db.Evolve.MaxStar);
if (starRow != null)
{
starText.text = "";
UiKit.Clear(starRow);
int max = h.isSoldier ? 3 : db.Evolve.MaxStar;
UiKit.StarRow(starRow, h.isSoldier ? h.rarity : hs.star, max, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, 56f);
}
var skills = new[] { db.Skills.Get(h.skillActive), db.Skills.Get(h.skillPassive) };
for (int i = 0; i < 2; i++)
{
int cap = SkillRules.Cap(db, h, hs, i); // 등급 + ★3 액티브 +1(PD #1093 B)
var sk = skills[i];
if (sk == null)
{
skillName[i].text = h.isSoldier ? "병사 유닛 — 스킬 없음" : "—";
skillDesc[i].text = "";
skillChip[i].transform.parent.gameObject.SetActive(false);
continue;
}
skillChip[i].transform.parent.gameObject.SetActive(true);
skillName[i].text = sk.nameKo + (string.IsNullOrEmpty(sk.hanja) ? "" : "(" + sk.hanja + ")") + " · " + (sk.kind == "active" ? "액티브" : "패시브");
skillDesc[i].text = SkillInfoDb.DisplayDesc(sk);
int slv = hs != null && hs.skillLv != null && hs.skillLv.Length > i ? hs.skillLv[i] : 1;
skillChip[i].text = "Lv " + slv + "/" + cap + (sk.kind == "active" ? " · 쿨 " + sk.cooldown + "턴" : (hs != null && SkillRules.PassiveMult(db, hs.star) > 1f ? " · ★×" + SkillRules.PassiveMult(db, hs.star).ToString("0.##") : ""));
}
bool canSkill = !h.isSoldier && hs != null && (SkillRules.CanLevelUp(db, S.Save, h, hs, 0) == null || SkillRules.CanLevelUp(db, S.Save, h, hs, 1) == null);
UiKit.LabelOf(skillBtn).text = "스킬 레벨업\n<size=22>" + (h.isSoldier ? "병사 = 스킬 없음" : "스킬서 " + S.Save.inv.books.skill) + "</size>";
skillBtn.GetComponent<Image>().color = canSkill ? UiKit.Red : UiKit.Disabled;
// 레벨 · 승급 — 구조적으로 불가(최고 · 병사) = 회색 · 조건 부족 = 원래 색 + 누르면 사유 토스트
int lvCap = HeroRules.LevelCap(db, S.Save);
var row = db.Levels.Get(lv.lv);
lvText.text = "Lv " + lv.lv + " / " + lvCap + (lvCap < db.Levels.MaxLevel ? "" : "");
float pct = row != null && row.expToNext > 0 ? lv.exp / (float)row.expToNext : 1f;
expText.text = lv.lv >= db.Levels.MaxLevel ? "최고 레벨" : pct >= 1f ? "경험치 100% ↑" : "경험치 " + (pct * 100f).ToString("0.00") + "%";
UiKit.SetBar(expBar, pct);
var lvWhy = HeroRules.CanLevelUp(db, S.Save, lv);
UiKit.LabelOf(levelBtn).text = "레벨업\n<size=24>" + (row != null && lv.lv < db.Levels.MaxLevel ? "골드 " + row.goldCost.ToString("N0") : "최고") + "</size>";
levelBtn.GetComponent<Image>().color = lvWhy == null ? UiKit.Red : UiKit.Disabled;
bool maxLv = lv.lv >= db.Levels.MaxLevel || row == null;
float pct = row != null && row.expToNext > 0 ? Mathf.Clamp01(lv.exp / (float)row.expToNext) : 1f;
lvBadge.text = lv.lv.ToString();
lvText.text = "Lv " + lv.lv + " / " + lvCap;
expText.text = maxLv ? "최고 레벨" : "경험치 " + (pct * 100f).ToString("0.##") + "%";
expFill.sizeDelta = new Vector2(458f * pct, expFill.sizeDelta.y);
expFill.gameObject.SetActive(pct > 0.01f);
levelImg.color = Col(maxLv ? "disabled" : "white");
levelLabel.color = levelCost.color = Col(maxLv ? "disabled_text" : "text_on_green");
levelCost.text = maxLv ? "최고" : row.goldCost.ToString("N0");
goldIcon.gameObject.SetActive(!maxLv);
var promoWhy = HeroRules.CanPromote(db, h, hs);
var gearList = h.isSoldier ? null : db.Heroes.GearFor(h, hs.grade);
UiKit.LabelOf(promoteBtn).text = h.isSoldier ? "승급\n<size=24>병사 없음</size>"
: gearList == null ? "승급\n<size=24>최고 등급</size>"
: "승급\n<size=24>장비 " + HeroRules.EquippedCount(hs) + "/" + gearList.Count + "칸</size>";
promoteBtn.GetComponent<Image>().color = promoWhy == null ? UiKit.Red : UiKit.Disabled;
var gearList = h.isSoldier || hs == null ? null : db.Heroes.GearFor(h, hs.grade);
promoteSub.text = h.isSoldier ? "병사 없음" : gearList == null ? "최고 등급" : "장비 " + HeroRules.EquippedCount(hs) + "/" + gearList.Count;
promoteImg.color = Col(gearList == null ? "disabled" : "white");
evolveDot.gameObject.SetActive(!h.isSoldier && hs != null && evolveDot.sprite != null && HeroRules.CanEvolve(db, h, hs) == null);
int need2 = HeroRules.ShardsForNext(db, hs);
if (h.isSoldier)
// 이름 띠
SetIcon(flagIcon, UISkin.Icon("flag_" + h.faction) != null ? "flag_" + h.faction : "flag_other", 1f);
SetIcon(clsIcon, UiKit.TypeIconId(h.type), 1f);
nameText.text = h.nameKo;
typeText.text = UnitTypeInfo.Korean(h.type);
PlaceX(typeText, 344f + nameText.preferredWidth + 24f);
UiKit.Clear(starRow);
int max = h.isSoldier ? 3 : db.Evolve.MaxStar;
int n = h.isSoldier ? h.rarity : hs != null ? hs.star : 1;
for (int i = 0; i < max; i++) Ico(starRow, i < n ? "star_l" : "star_off", i * 58f, 0f, 58f, 60f, i < n ? 3f : 0f);
// 스킬 — 상한(등급·★3) 또는 최고 = 회색 「최대」 · 그 밖 = 보라 레벨업(스킬서 · 골드)
string tk = UiKit.TypeKey(h.type);
for (int i = 0; i < 2; i++)
{
shardText.text = "병사 유닛 — 진화 없음 (중복 = 골드)";
UiKit.SetBar(shardBar, 0f);
UiKit.LabelOf(evolveBtn).text = "진화\n<size=24>없음</size>";
evolveBtn.GetComponent<Image>().color = UiKit.Disabled;
}
else
var sk = SkillRules.Skill(db, h, i);
bool has = sk != null && !h.isSoldier && hs != null;
SetIcon(skFrame[i], tk != null ? "skill_" + tk + (i == 0 ? "_a" : "_p") : null, has ? 1f : 0.5f);
SetIcon(skCls[i], UiKit.TypeIconId(h.type), has ? 1f : 0.5f);
skLv[i].transform.parent.gameObject.SetActive(has);
skBtn[i].gameObject.SetActive(has);
if (i == 0) coolChip.SetActive(has);
if (!has)
{
shardText.text = need2 > 0 ? hs.shards + " / " + need2 : hs.shards + " · 최고 ★";
UiKit.SetBar(shardBar, need2 > 0 ? hs.shards / (float)need2 : 1f);
UiKit.LabelOf(evolveBtn).text = hs.star >= db.Evolve.MaxStar ? "진화\n<size=24>최고 ★</size>" : "진화\n<size=24>다음 ★" + (hs.star + 1) + "</size>";
evolveBtn.GetComponent<Image>().color = HeroRules.CanEvolve(db, h, hs) == null ? UiKit.Red : UiKit.Disabled;
skName[i].text = h.isSoldier ? "스킬 없음" : "—";
skHanja[i].text = "";
skDesc[i].text = h.isSoldier ? "병사 유닛은 스킬이 없습니다" : "";
continue;
}
int slv = SkillRules.Level(hs, i), cap = SkillRules.Cap(db, h, hs, i);
skName[i].text = sk.nameKo;
skHanja[i].text = sk.hanja ?? "";
skHanja[i].color = Col(G + "label");
PlaceX(skHanja[i], 200f + skName[i].preferredWidth + 8f);
skDesc[i].text = SkillInfoDb.DisplayDesc(sk);
skLv[i].text = "Lv " + slv + "/" + cap;
if (i == 0) coolText.text = "쿨 " + sk.cooldown + "턴";
bool top = slv >= cap;
UISkin.Apply(skBtn[i], top ? "btn_gray" : "btn_purple");
skBtn[i].color = Color.white;
skBtnText[i].text = top ? "최대" : "레벨업";
skBtnText[i].color = Col(top ? "text_max" : "white");
skBtnSub[i].gameObject.SetActive(top);
skBtnSub[i].text = slv >= sk.lvMax ? "최고 레벨" : "등급 상한";
skCost[i].SetActive(!top);
var cost = db.Skills.CostTo(slv + 1);
skBooks[i].text = cost != null ? cost.books.ToString() : "-";
skGold[i].text = cost != null ? cost.gold.ToString("N0") : "-";
}
}
/// <summary>메뉴 4버튼: 0 정보(상세정보 + 도감) · 1 진화(조각 팝업) · 2 각성 · 3 외형(준비 중).</summary>
public void OnMenu(int i)
{
switch (i)
{
case 0: ShowStats(); break;
case 1: ShowEvolve(); break;
case 2: Root.ComingSoon("각성"); break;
case 3: Root.ComingSoon("외형"); break;
}
}
/// <summary>스킬 행 레벨업 = 그 스킬만(스킬서 + 골드 · 불가 = 사유 토스트).</summary>
public void OnSkillRow(int i)
{
var h = S.Def(id);
var hs = S.Save.Hero(id);
if (h == null || hs == null || h.isSoldier)
{
Root.Toast("병사 유닛은 스킬이 없습니다");
return;
}
var sk = SkillRules.Skill(S.Db, h, i);
if (sk == null) return;
var r = SkillRules.LevelUp(S.Db, S.Save, h, hs, i);
if (r == null) S.Commit();
Refresh();
Root.Toast(r ?? sk.nameKo + " Lv " + SkillRules.Level(hs, i) + "!");
}
/// <summary>진화 팝업 = ★ 줄 + 영웅 조각 바 + 다음 ★ · 진화 버튼(기존 진화 규칙).</summary>
public void ShowEvolve()
{
var h = S.Def(id);
var hs = S.Save.Hero(id);
if (h == null || hs == null || h.isSoldier)
{
Root.Toast("병사 유닛 — 진화 없음 (중복 = 골드)");
return;
}
var db = S.Db;
int need = HeroRules.ShardsForNext(db, hs);
bool top = hs.star >= db.Evolve.MaxStar;
var why = HeroRules.CanEvolve(db, h, hs);
var c = Root.ModalCustom("진화 — " + h.nameKo, 600f,
new OutGameRoot.ModalButton(top ? "최고 ★" : "진화", why == null ? UiKit.Red : UiKit.Disabled, OnEvolve, why != null),
new OutGameRoot.ModalButton("닫기", UiKit.Dark, null));
UiKit.StarRow(c, hs.star, db.Evolve.MaxStar, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -4f), 64f);
UiKit.IconOrDot(c, "cur_shard", "조", UiKit.Purple, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(20f, -90f), 60f);
UiKit.Label(c, "영웅 조각", 34, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(92f, -90f), new Vector2(400f, 60f));
var bar = UiKit.Bar("ShardBar", c, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -164f), new Vector2(820f, 46f), UiKit.Slot, UiKit.Purple);
UiKit.SetBar(bar, need > 0 ? Mathf.Clamp01(hs.shards / (float)need) : 1f);
UiKit.Title(UiKit.Label(bar.parent, need > 0 ? hs.shards + " / " + need : hs.shards + " · 최고 ★", 30, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(800f, 46f)), true);
UiKit.Label(c, top ? "최고 ★ 달성" : "다음 ★" + (hs.star + 1) + " · 필요 조각 " + need + (why != null ? "\n" + why : ""), 28, UiKit.InkSoft, TextAnchor.UpperCenter,
new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -226f), new Vector2(840f, 84f), false);
}
void OnSlot(int i)
@ -459,7 +694,8 @@ namespace TKS.OutGame
Root.Toast(why ?? S.Def(id).nameKo + " 진화 → ★" + S.Save.Hero(id).star);
}
void ShowStats()
/// <summary>정보 = 상세정보(스탯 내역) + 도감 버튼.</summary>
public void ShowStats()
{
var h = S.Def(id);
var st = S.StatsOf(id);
@ -475,7 +711,9 @@ namespace TKS.OutGame
"보물(수집품·세트) HP +" + st.treasureHpPct.ToString("0.#") + "% · 공 +" + st.treasureAtkPct.ToString("0.#") + "% · 방 +" + st.treasureDefPct.ToString("0.#") + "%\n\n" +
"<size=40>HP " + st.Hp + " · 공 " + st.Atk + " · 방 " + st.Def + "</size>\n" +
"전투력 " + st.power.ToString("N0") + " = HP/" + e.PowerHpDiv + " + 공×" + e.PowerAtkW + " + 방×" + e.PowerDefW;
Root.Modal("상세정보 — " + h.nameKo, body, new OutGameRoot.ModalButton("닫기", UiKit.Dark, null));
Root.Modal("상세정보 — " + h.nameKo, body,
new OutGameRoot.ModalButton("도감", UiKit.Dark, () => ShowCodex(Root)),
new OutGameRoot.ModalButton("닫기", UiKit.Dark, null));
}
/// <summary>도감 = 전 영웅·병사(미보유 회색).</summary>

View File

@ -24,13 +24,23 @@ namespace TKS.OutGame
/// <summary>뒤로 가기를 화면 안에서 처리했으면 true.</summary>
public virtual bool OnBack() { return false; }
/// <summary>제목 띠(재화 바 아래) · back = 「◀」 버튼.</summary>
protected void Header(string title, string sub, bool back)
/// <summary>제목 띠(재화 바 아래 · 팩 PanelFrame03 진남 + 왼쪽 붙은 뒤로 · UI 상업용 재설계 공통 크롬) · back = 뒤로 버튼 · sub 빈 문자열 = 부제 없음.</summary>
protected RectTransform Header(string title, string sub, bool back)
{
var band = UiKit.TopBand("Header", Rt, OutGameRoot.TopBarH, OutGameRoot.HeaderH);
UiKit.Paint(band, UiKit.Header, false);
if (UISkin.Has("header_bar")) UiKit.Skin(band, "header_bar", UiKit.Col("chrome_header", UiKit.Header));
else UiKit.Paint(band, UiKit.Header, false);
float x = 36f;
if (back)
if (back && UISkin.Has("back_flush"))
{
var bb = UiKit.Rect("Back", band, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), Vector2.zero, new Vector2(132f, 90f));
UiKit.Skin(bb, "back_flush", UiKit.White, true);
bb.gameObject.AddComponent<Button>().onClick.AddListener(() => Root.Back());
if (UiKit.Icon(bb, "back", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(-5f, 0f), 62f) == null)
UiKit.Title(UiKit.FillLabel(bb, "◀", 44, UiKit.White), true);
x = 156f;
}
else if (back)
{
var b = UiKit.Btn("Back", band, "◀", 44, UISkin.On ? UiKit.Red : UiKit.Dark, UiKit.White, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(24f, 0f), new Vector2(100f, 80f), () => Root.Back());
if (UiKit.Icon(b.transform, "arrow_back", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 6f), 50f) != null) UiKit.LabelOf(b).text = "";
@ -38,7 +48,9 @@ namespace TKS.OutGame
}
var t = UiKit.Title(UiKit.Label(band, title, 52, UiKit.HeaderText, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(x, 0f), new Vector2(360f, 90f)), true);
t.horizontalOverflow = HorizontalWrapMode.Overflow;
if (!string.IsNullOrEmpty(sub))
UiKit.Label(band, sub, 30, UiKit.HeaderSub, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(x + t.preferredWidth + 18f, -4f), new Vector2(700f, 60f), false);
return band;
}
/// <summary>
@ -103,6 +115,7 @@ namespace TKS.OutGame
Text staminaTimer;
readonly List<Image> tabImages = new List<Image>();
readonly List<Text> tabTexts = new List<Text>();
readonly List<RectTransform> tabIcons = new List<RectTransform>();
GameObject modal;
GameObject toast;
@ -257,7 +270,17 @@ namespace TKS.OutGame
for (int i = 0; i < tabImages.Count; i++)
{
bool on = scr.Tab == i;
if (UISkin.On) tabImages[i].gameObject.SetActive(on);
if (UISkin.On)
{
tabImages[i].gameObject.SetActive(on);
tabTexts[i].gameObject.SetActive(on);
var ic = i < tabIcons.Count ? tabIcons[i] : null;
if (ic != null)
{
ic.sizeDelta = Vector2.one * (on ? 118f : 104f);
ic.anchoredPosition = new Vector2(0f, on ? 48f : 30f);
}
}
else tabImages[i].color = on ? UiKit.Red : UiKit.Dark;
}
scr.OnShow();
@ -346,15 +369,41 @@ namespace TKS.OutGame
void BuildCurrencyBar()
{
barRoot = UiKit.TopBand("CurrencyBar", TopRoot, 0f, TopBarH);
UiKit.Paint(barRoot, UISkin.On ? UiKit.BarColor : new Color(0.15f, 0.11f, 0.09f), false, true);
goldText = Pill(0, "cur_gold", "골", "골드", UiKit.Coin, () => Toast("골드 = 스테이지 보상 · 거래 > 상점 · (구매 상점 준비 중)"));
gemText = Pill(1, "cur_gem", "보", "보옥", UiKit.Jade, () => Toast("보옥 구매(과금) = 준비 중 · 첫 클리어 · 보스 보상"));
staminaText = Pill(2, "cur_stamina", "군", "군량", UiKit.Grain, StaminaModal);
// 테마 = 띠 없이 칩만(화면 바탕이 비침 · 목업 hero_detail) · 투명 판은 아래 클릭 막기용
UiKit.Paint(barRoot, UISkin.On ? Color.clear : new Color(0.15f, 0.11f, 0.09f), false, true);
goldText = Pill(0, "cur_gold", "plus", "골", "골드", UiKit.Coin, () => Toast("골드 = 스테이지 보상 · 거래 > 상점 · (구매 상점 준비 중)"));
gemText = Pill(1, "cur_gem", "plus_gem", "보", "보옥", UiKit.Jade, () => Toast("보옥 구매(과금) = 준비 중 · 첫 클리어 · 보스 보상"));
staminaText = Pill(2, "cur_stamina", "plus_stamina", "군", "군량", UiKit.Grain, StaminaModal);
staminaTimer = staminaText.transform.parent.Find("Name").GetComponent<Text>();
}
/// <summary>재화 칩(팩 ResourceBar · 아이콘 = 테마 cur_* · 「+」 = 팩 노란 단추).</summary>
Text Pill(int i, string icon, string mark, string name, Color c, Action plus)
/// <summary>재화 칩(목업 = 칩 280×58 · 아이콘 84 가 칩 왼쪽 밖으로 · 값 38 · 재화별 「+」 62×64 · x = 40 + i × 350).</summary>
Text Pill(int i, string icon, string plusSkin, string mark, string name, Color c, Action plus)
{
if (UISkin.Has("pill") && UISkin.Has("plus"))
{
float x = 40f + i * 350f;
var p = UiKit.Rect("Pill" + i, barRoot, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(x + 30f, -34f), new Vector2(280f, 58f));
UiKit.Skin(p, "pill", UiKit.Col("chrome_pill", UiKit.BarColor));
var ic = UiKit.IconOrDot(p, icon, mark, c, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(-36f, 14f), 84f);
var sh = ic.gameObject.AddComponent<Shadow>();
sh.effectColor = UiKit.Col("icon_shadow", Color.black);
sh.effectDistance = new Vector2(0f, -3f);
var n = UiKit.Label(p, name, 22, UISkin.Col("bar_text_off", UiKit.White), TextAnchor.MiddleLeft, new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(54f, 0f), new Vector2(140f, 30f), false);
n.gameObject.name = "Name";
n.text = "";
var v = UiKit.Title(UiKit.Label(p, "", 38, UISkin.Col("bar_text", UiKit.White), TextAnchor.MiddleRight, new Vector2(0f, 1f), new Vector2(1f, 0.5f), new Vector2(214f, -29f), new Vector2(170f, 60f)), true);
v.horizontalOverflow = HorizontalWrapMode.Overflow;
var pb = UiKit.Rect("Plus", p, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(232f, 4f), new Vector2(62f, 64f));
UiKit.Skin(pb, UISkin.Has(plusSkin) ? plusSkin : "plus", Color.white, true);
pb.gameObject.AddComponent<Button>().onClick.AddListener(() => plus());
return v;
}
return PillOld(i, icon, mark, name, c, plus);
}
/// <summary>테마 없을 때(도형) 재화 칩.</summary>
Text PillOld(int i, string icon, string mark, string name, Color c, Action plus)
{
float w = 336f;
var p = UiKit.Rect("Pill" + i, barRoot, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(18f + i * (w + 18f), 0f), new Vector2(w, 84f));
@ -383,7 +432,7 @@ namespace TKS.OutGame
gemText.text = d.gem.ToString("N0");
staminaText.text = d.stamina + "/" + EconomyRules.StaminaMax(Session.Db, d);
int left = Session.StaminaSecondsToNext();
staminaTimer.text = left > 0 ? (left / 60).ToString("00") + ":" + (left % 60).ToString("00") : "군량";
staminaTimer.text = left > 0 ? (left / 60).ToString("00") + ":" + (left % 60).ToString("00") : UISkin.Icon("cur_stamina") != null ? "" : "군량";
}
// ---------- 하단 탭 ----------
@ -395,8 +444,8 @@ namespace TKS.OutGame
bar.anchorMax = new Vector2(1f, 0f);
bar.offsetMin = new Vector2(0f, 0f);
bar.offsetMax = new Vector2(0f, TabBarH);
UiKit.Paint(bar, UISkin.On ? UiKit.BarColor : UiKit.Dark, false, true);
if (UISkin.On) UiKit.Box("TopLine", bar, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), Vector2.zero, new Vector2(Reference.x * 2f, 6f), UiKit.Line, false);
if (UISkin.Has("tabbar")) UiKit.Skin(bar, "tabbar", UiKit.Col("chrome_tab", UiKit.BarColor), true);
else UiKit.Paint(bar, UiKit.Dark, false, true);
string[] names = { "영웅", "보물", "내정", "동맹", "거래" };
string[] marks = { "영", "보", "내", "잠", "거" };
string[] icons = { "tab_hero", "tab_treasure", "tab_domestic", "tab_alliance", "tab_trade" };
@ -415,16 +464,21 @@ namespace TKS.OutGame
Image img;
if (UISkin.On)
{
// 선택 탭 = 팩 둥근 판(붉은 옻칠 틴트 · 위로 솟음) · 아이콘 = 테마 tab_*(흰 실루엣)
var focus = UiKit.Fill("Focus", cell, 8f, 8f, -18f, 6f);
img = UiKit.Skin(focus, "tab_focus", UiKit.Red);
var ic = UiKit.Icon(cell, icons[i], new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -14f), 74f);
// 선택 탭 = 팩 TabFocus 솟음 판 204×168(붉은 옻칠 + 광택) · 아이콘 104(선택 118 · 위로) · 라벨 = 선택 탭만(목업 hero_detail)
var focus = UiKit.Rect("Focus", cell, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), Vector2.zero, new Vector2(204f, 168f));
img = UiKit.Skin(focus, "tab_focus", UiKit.Col("tab_focus", UiKit.Red));
if (UISkin.Has("tab_focus_light"))
UiKit.Skin(UiKit.Rect("Light", focus, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -6f), new Vector2(188f, 70f)), "tab_focus_light", UiKit.Col("tab_focus_light", UiKit.White));
var ic = UiKit.Icon(cell, icons[i], new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 30f), 104f);
if (ic != null)
{
if (i == 3) ic.color = off;
UISkin.Stroke(ic, UiKit.Line, 3f);
if (i == 3) ic.color = UiKit.Col("tab_locked", off);
var sh = ic.gameObject.AddComponent<Shadow>();
sh.effectColor = UiKit.Col("icon_shadow", Color.black);
sh.effectDistance = new Vector2(0f, -4f);
}
else UiKit.FillLabel(UiKit.Dot("Icon", cell, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -18f), 66f, UISkin.Col("tab_off", UiKit.Dark)).transform, marks[i], 32, UiKit.White);
tabIcons.Add(ic != null ? ic.rectTransform : null);
}
else
{
@ -433,8 +487,10 @@ namespace TKS.OutGame
var dot = UiKit.Dot("Icon", cell, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -18f), 66f, i == 3 ? new Color(0.35f, 0.3f, 0.28f) : new Color(0.38f, 0.31f, 0.28f));
UiKit.FillLabel(dot.transform, marks[i], 32, i == 3 ? new Color(0.7f, 0.66f, 0.62f) : UiKit.White);
}
var t = UiKit.Label(cell, names[i], 32, i == 3 ? off : UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 14f), new Vector2(200f, 48f));
if (UISkin.On) UiKit.Title(t, i != 3);
var t = UISkin.On
? UiKit.Label(cell, names[i], 36, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0.5f), new Vector2(0f, 36f), new Vector2(204f, 50f))
: UiKit.Label(cell, names[i], 32, i == 3 ? off : UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 14f), new Vector2(200f, 48f));
if (UISkin.On) UiKit.Title(t, true);
tabImages.Add(img);
tabTexts.Add(t);
}

View File

@ -361,20 +361,27 @@ namespace TKS.OutGame
return Dark;
}
/// <summary>병종 아이콘 id(테마 · 신규 제작 cls_*).</summary>
public static string TypeIconId(UnitType t)
/// <summary>병종 키(아이콘 id 접미 · cls_&lt;키&gt; · skill_&lt;키&gt;_a|p).</summary>
public static string TypeKey(UnitType t)
{
switch (t)
{
case UnitType.Infantry: return "cls_infantry";
case UnitType.Spear: return "cls_spear";
case UnitType.Cavalry: return "cls_cavalry";
case UnitType.Archer: return "cls_archer";
case UnitType.Strategist: return "cls_strategist";
case UnitType.Infantry: return "infantry";
case UnitType.Spear: return "spear";
case UnitType.Cavalry: return "cavalry";
case UnitType.Archer: return "archer";
case UnitType.Strategist: return "strategist";
}
return null;
}
/// <summary>병종 아이콘 id(테마 · 신규 제작 cls_*).</summary>
public static string TypeIconId(UnitType t)
{
var k = TypeKey(t);
return k != null ? "cls_" + k : null;
}
/// <summary>신규 제작(B) 치환이 들어온 아이콘인가(임시 팩 아이콘이 뜻과 어긋나는 곳은 그때만 아이콘을 쓴다 · 예: 병종).</summary>
public static bool HasFinalIcon(string id)
{

View File

@ -73,6 +73,17 @@ namespace TKS.OutGame
yield return Shot(cam, "hero_list", W, H);
root.ShowHero(id);
yield return Shot(cam, "hero_detail", W, H);
// 영웅 상세 등급 변형(UI 상업용 재설계 ① · 목업 hero_detail_5등급 대조 · 메모리 세이브만) = 그 영웅 장비 세트의 등급 순서대로
string g0 = hs.grade;
foreach (var set in h.gear)
{
if (set.grade == g0) continue;
hs.grade = set.grade;
root.HeroDetail.Refresh();
yield return Shot(cam, "hero_detail_" + set.grade, W, H);
}
hs.grade = g0;
root.HeroDetail.Refresh();
root.Show(root.Formation);
yield return Shot(cam, "formation", W, H);
root.Show(root.Trade);

View File

@ -1083,6 +1083,42 @@ namespace TKS.Tests
Assert.AreEqual(og.Session.Db.Economy.ShopSlots, og.Trade.ShopCardCount, "장비 칸 수는 그대로");
}
[UnityTest]
public IEnumerator HeroDetail_Redesign_SkillRow_Swipe_Menu()
{
// UI 상업용 재설계 ① 영웅 상세(#1104): 스킬 행 레벨업 = 그 스킬만 · 일러 밀기 80 px 이상 = 영웅 전환 · 메뉴 진화 = 팝업 · 각성 = 준비 중
var og = OutGameRoot.Instance;
Assert.IsNotNull(og);
og.ShowLobby();
var id = og.Session.Db.Economy.StartHeroes[0];
og.ShowHero(id);
yield return null;
var hd = og.HeroDetail;
var h = og.Session.Def(id);
var hs = og.Session.Save.Hero(id);
int a0 = SkillRules.Level(hs, 0), p0 = SkillRules.Level(hs, 1);
bool can = SkillRules.CanLevelUp(og.Session.Db, og.Session.Save, h, hs, 0) == null;
hd.OnSkillRow(0);
Assert.AreEqual(can ? a0 + 1 : a0, SkillRules.Level(og.Session.Save.Hero(id), 0), "액티브 행 레벨업");
Assert.AreEqual(p0, SkillRules.Level(og.Session.Save.Hero(id), 1), "패시브는 그대로");
Assert.IsFalse(og.ModalVisible, "행 레벨업 = 팝업 없이");
hd.Swipe(-20f);
Assert.AreEqual(id, hd.HeroId, "80 px 미만 = 무시");
if (og.Session.Owned(false).Count > 1)
{
hd.Swipe(-200f);
Assert.AreNotEqual(id, hd.HeroId, "왼쪽 밀기 = 다음 영웅");
hd.Swipe(200f);
Assert.AreEqual(id, hd.HeroId, "오른쪽 밀기 = 이전 영웅");
}
hd.OnMenu(1);
Assert.IsTrue(og.ModalVisible, "진화 = 조각 팝업");
og.CloseModal();
hd.OnMenu(2);
Assert.IsTrue(og.ModalVisible, "각성 = 준비 중");
og.CloseModal();
}
// ---------- 영웅 56명 확장(PD #1096) — 신규 키 4종 · 기존 키 조건 열 ----------
[UnityTest]