using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace TKS.OutGame { /// /// 영웅·병사 카드(가로형 · UI 상업용 재설계 ② · 설계서 §3② · 팩 CardFrame01 5층 등급색) — 초상(뒤 빛) · 병종 원 · Lv 배지 · 세력 깃발 · 출전 꼬리 칩 · /// 아래 이름 띠(이름 · ★ · 등급 칩) · 미보유 = 빈 카드(문양 + 초상 어둡게). 목록 · 편성 보유 행 · 도감 공용. /// public static class HeroCards { public static Image Card(Transform content, OutGameSession s, HeroDb.Hero h, bool deployed, bool owned, System.Action onTap) { var cell = UiKit.Cell(content, "Card_" + h.id, Color.clear, onTap); cell.sprite = null; // 바닥 = 클릭 판(투명) · 그림은 층으로 var lv = s.Save.Level(h.id); var hs = s.Save.Hero(h.id); string g = h.isSoldier || hs == null || string.IsNullOrEmpty(hs.grade) ? "white" : hs.grade; string G = "grade_" + g + "_"; var t = cell.transform; if (owned) { UiKit.Layer(t, "Bg", "card_bg", UiKit.Col(G + "card_bg", UiKit.Paper)); UiKit.Layer(t, "Grad", "card_grad", UiKit.Col(G + "card_grad", UiKit.White), 0f, 6f, 0f, 0f); var glow = UiKit.Skin(UiKit.Rect("Glow", t, new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(72f, 6f), new Vector2(190f, 190f)), "card_glow", UiKit.Col(G + "card_glow", UiKit.White)); glow.enabled = UISkin.Has("card_glow"); } else { UiKit.Layer(t, "Bg", "card_empty", UiKit.Col("card_empty", UiKit.Slot)); var pat = UiKit.Layer(t, "Pattern", "card_empty_pattern", UiKit.Col("card_empty_pattern", UiKit.Slot), 6f, 6f, 6f, 6f); pat.type = Image.Type.Tiled; } UiKit.Portrait("Face", t, h, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(12f, 6f), new Vector2(120f, 116f), false, 2f, Color.clear); if (!owned) UiKit.Paint(UiKit.Rect("Dim", t, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(12f, 6f), new Vector2(120f, 116f)), UiKit.Col("card_dim", UiKit.Dark), false); var band = UiKit.Rect("Band", t, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), Vector2.zero, new Vector2(0f, 62f)); band.anchorMin = new Vector2(0f, 0f); band.anchorMax = new Vector2(1f, 0f); band.sizeDelta = new Vector2(0f, 62f); UiKit.Skin(band, "card_bottom", UiKit.Col("card_bottom", UiKit.Dark)).enabled = UISkin.Has("card_bottom"); UiKit.Layer(t, "Border", "card_border", UiKit.Col(owned ? "card_border" : "card_empty_border", UiKit.Line)); int star = hs != null ? hs.star : h.rarity; UiKit.StarRow(t, star, star, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(16f, 8f), 24f); var name = UiKit.Title(UiKit.Label(t, h.nameKo, 40, owned ? UiKit.White : UiKit.InkSoft, TextAnchor.MiddleLeft, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(142f, 4f), new Vector2(260f, 56f)), true); name.horizontalOverflow = HorizontalWrapMode.Overflow; // 위 줄: 병종 원 · Lv 배지 · 세력 깃발 · (출전 꼬리 칩) // 병과 = 단색 심볼만(배경 틀 없음 · PD 2026-11-09) var ring = UiKit.Rect("Type", t, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(142f, -12f), new Vector2(52f, 52f)); var cls = UiKit.Icon(ring, UiKit.TypeSymbolId(h.type), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 50f); if (cls == null) UiKit.Title(UiKit.FillLabel(ring, UnitTypeInfo.Mark(h.type), 26, UiKit.White), true); if (owned) { var badge = UiKit.Icon(t, "lv_badge_s", new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(202f, -8f), 56f); var lt = UiKit.Title(UiKit.Label(badge != null ? badge.transform : t, (lv != null ? lv.lv : 1).ToString(), 26, UiKit.White, TextAnchor.MiddleCenter, badge != null ? new Vector2(0.5f, 0.5f) : new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), badge != null ? new Vector2(0f, 2f) : new Vector2(230f, -36f), new Vector2(56f, 40f)), true); lt.horizontalOverflow = HorizontalWrapMode.Overflow; } UiKit.DropShadow(UiKit.FactionBadge(t, h.faction, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(264f, -12f), 46f), 2f); // 진영 원 배지(PD 2026-11-09) if (deployed) { var tail = UiKit.Skin(UiKit.Rect("Deployed", t, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-10f, -10f), new Vector2(104f, 46f)), "tail", UiKit.Col("tail_red", UiKit.Red)); UiKit.Title(UiKit.FillLabel(tail.transform, "출전", 26, UiKit.White), true); } if (owned && hs != null && !h.isSoldier && UISkin.Has("para_" + g)) { var chip = UiKit.Skin(UiKit.Rect("Grade", t, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-14f, 11f), new Vector2(66f, 40f)), "para_" + g, UiKit.Col(G + "chip", UiKit.White)); UiKit.Title(UiKit.FillLabel(chip.transform, UiKit.GradeKo(g), 24, UiKit.White), true); } if (owned && s.CanGrowAny(h.id) && UiKit.Icon(t, "new_dot", new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(14f, -10f), 34f) == null) UiKit.Dot("New", t, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(14f, -10f), 26f, UiKit.Red); return cell; } } /// /// 영웅 목록 세로 카드(PD 2026-11-09 레퍼런스 = 팩 데모 Collection_List · 칸 320×431) — 등급색 바탕(마스크) · 그라데이션 · 일러 뒤 빛 · 일러 · 아래 어두운 띠 · 테두리 · /// 이름(왼쪽 위) · 병종 원 · 레벨 배지(왼쪽 아래 · 최고 = 노랑) · ★(오른쪽 아래) · 미보유 = 빈 카드(문양 + 얼굴 실루엣). 수치 = 팩 프리팹 RectTransform 그대로. /// public static class CollectionCards { public static readonly Vector2 CellSize = new Vector2(320f, 431f); public static readonly Vector2 Spacing = new Vector2(22f, 20f); /// 크기 표 size_px(머리 크기 기준 표준 키)가 카드에서 차지할 px — 배율 = UnitPx ÷ size_px(모든 유닛 머리 크기 같음) · 머리 끝 위치(카드 위에서 px). const float UnitPx = 360f; // 450 → 360 = 유닛 20 % 작게(PD 2026-11-09 「현재 비율보다 20 % 정도 작게」) const float HeadTop = 50f; /// 걷는 유닛(보병·창병·궁병·책사) = 머리 끝을 더 내려 발이 카드 아래로 숨게(PD 2026-11-09). const float FootDrop = 36f; /// 말 탄 유닛(크기 표 mounted) = 걷는 유닛보다 작게 · 말 얼굴이 카드 오른쪽 경계에 닿을 만큼(PD 2026-11-09 0.72 → 「조금 더 크게 · 내려도」). const float MountedScale = 0.85f; /// 말 탄 유닛 머리 끝 내림(카드 px · PD 2026-11-09). const float MountedDrop = 24f; /// 말 탄 유닛 전신(발 ~ 머리 끝) 높이 상한(카드 px) — 크기 표 실측이 튀는 그림(예 여포 size_px 449)도 다른 기병과 비슷하게. const float MountedMaxBody = 460f; /// 눈 줄(카드 위에서 px · 크기 표 eye_up_px 가 있는 유닛) — 머리 끝 맞춤이면 관·투구가 낮은 유닛(전위·태사자)의 얼굴이 위로 뜸 → /// 눈 높이를 맞춘다(PD 2026-11-10 「너무 위쪽에 등장하는 영웅은 내려」) · 값 = PD 확인 기존 20종의 머리 끝 맞춤 때 눈 높이 중앙값(걷는 216.5 · 기병 210.3). const float EyeLine = 216f; const float EyeLineMounted = 210f; /// false = 미보유(PD 2026-11-10 「유닛 목록에 장수들의 모습이 보이도록」) — 일러를 테마 card_locked_art 로 조금 어둡게 · 「미보유」 꼬리 칩 · 레벨 배지·성장 점 없음 public static Image Card(Transform content, OutGameSession s, HeroDb.Hero h, bool deployed, System.Action onTap, bool owned = true) { var cell = UiKit.Cell(content, "Card_" + h.id, Color.clear, onTap); cell.sprite = null; // 바닥 = 클릭 판(투명) · 그림은 층으로 var t = cell.transform; var lv = s.Save.Level(h.id); var hs = s.Save.Hero(h.id); string g = h.isSoldier || hs == null || string.IsNullOrEmpty(hs.grade) ? "white" : hs.grade; string G = "grade_" + g + "_"; // 바탕(마스크) 안 = 그라데이션 · 빛 · 일러 · 아래 띠(카드 둥근 모양으로 잘림) var bg = UiKit.Layer(t, "Bg", "card_bg", UiKit.Col(G + "card_bg", UiKit.Paper), 2f, 2f, 2f, 2f); bg.gameObject.AddComponent().showMaskGraphic = true; var b = bg.transform; UiKit.Layer(b, "Gradient", "card_grad", UiKit.Col(G + "card_grad", UiKit.White), 0f, 0f, 0f, 44.6f); UiKit.Skin(UiKit.Rect("Glow", b, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(226f, 226f)), "card_glow", UiKit.Col(G + "card_glow", UiKit.White)).enabled = UISkin.Has("card_glow"); var sp = UiKit.ArtSprite(h.art); if (sp != null) { // 머리 크기 통일(PD 2026-11-09 「캐릭터 크기가 동일하게 보이도록」) = 배율 UnitPx ÷ 크기 표 size_px(눈 간격 기준 · 말 탄 유닛은 탄 사람 기준) · // 머리 끝(발 + bodyHeightPx · 무기 제외)을 카드 위 HeadTop 에 맞춤 → 다리·말은 아래 띠 뒤로 잘림(레퍼런스 흉상 느낌) · 메타가 없으면 칸 맞춤 var a = UnitArt.Load(h.art); var m = a != null ? a.Meta : null; float body = a != null ? a.BodyPx : 0f; float size = a != null && a.SizePx > 0.5f ? a.SizePx : body; Image art; if (body > 0.5f && m.pivot != null && m.pivot.Length > 1) { float hgt = sp.rect.height; bool mounted = a.Mounted; float k = UnitPx / size * (mounted ? MountedScale : 1f); if (mounted) k = Mathf.Min(k, MountedMaxBody / body); float pivotY, y; if (a.EyeUpPx > 0.5f) { pivotY = Mathf.Clamp01((m.pivot[1] * hgt + a.EyeUpPx) / hgt); // 눈 줄 = 프레임 아래에서의 비율 y = mounted ? EyeLineMounted : EyeLine; } else { pivotY = Mathf.Clamp01((m.pivot[1] * hgt + body) / hgt); // 머리 끝 = 프레임 아래에서의 비율(눈 높이 없는 그림) y = HeadTop + (mounted ? MountedDrop : FootDrop); } art = UiKit.Rect("Character", b, new Vector2(0.5f, 1f), new Vector2(0.5f, pivotY), new Vector2(0f, -y), sp.rect.size * k).gameObject.AddComponent(); } else { art = UiKit.Rect("Character", b, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -56f), new Vector2(376f, 368f)).gameObject.AddComponent(); art.preserveAspect = true; } art.sprite = sp; art.raycastTarget = false; if (!owned) art.color = UiKit.Col("card_locked_art", UiKit.InkSoft); } else { var dot = UiKit.Dot("Shape", b, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -10f), 200f, UiKit.TypeColor(h.type)); UiKit.Title(UiKit.FillLabel(dot.transform, UnitTypeInfo.Mark(h.type), 100, UiKit.White), true); } var bottom = UiKit.Rect("Bottom", b, new Vector2(0.5f, 0f), new Vector2(0.5f, 0.5f), new Vector2(0f, 21.7f), new Vector2(0f, 91.5f)); bottom.anchorMin = new Vector2(0f, 0f); bottom.anchorMax = new Vector2(1f, 0f); bottom.sizeDelta = new Vector2(52.6f, 91.5f); UiKit.Skin(bottom, "card_bottom", UiKit.Col("card_bottom", UiKit.Dark)).enabled = UISkin.Has("card_bottom"); UiKit.Layer(t, "Border", "card_border", UiKit.Col("card_border", UiKit.Line)); // ★(오른쪽 아래 · 42×45 · 28 간격 겹침) int star = hs != null ? hs.star : h.rarity; for (int i = star - 1; i >= 0; i--) UiKit.Icon(t, "star_on", new Vector2(1f, 0f), new Vector2(1f, 1f), new Vector2(-15.5f - i * 28f, 64.8f), 44f); // 레벨 배지(왼쪽 아래 · 최고 레벨 = 노랑 · 미보유 = 없음) int lvn = lv != null ? lv.lv : 1; bool maxLv = s.Db.Levels != null && lvn >= s.Db.Levels.MaxLevel; var badge = owned ? UiKit.Icon(t, maxLv ? "lv_badge_max" : "lv_badge_s", new Vector2(0f, 0f), new Vector2(0.5f, 0.5f), new Vector2(33.9f, 36.7f), 79f) : null; if (badge != null) { var bt = UiKit.Title(UiKit.Label(badge.transform, lvn.ToString(), 30, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(1f, -3f), new Vector2(74f, 50f)), true); bt.horizontalOverflow = HorizontalWrapMode.Overflow; } // PD 커스터마이징(2026-11-09): 왼쪽 위 = 진영 깃발 · 그 오른쪽 = 이름(45 · 긴 이름 자동 축소) · 깃발 아래(왼쪽 가장자리) = 출전 · 오른쪽 위 = 병종 원(#35374E 58×60) // 진영 = 원 배지(위·촉·오·군 · PD 2026-11-09 · 구 세력 깃발 대체) UiKit.DropShadow(UiKit.FactionBadge(t, h.faction, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(44f, -42f), 62f), 3f); var name = UiKit.Title(UiKit.Label(t, h.nameKo, 45, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(80f, -40.5f), new Vector2(172f, 66f)), true); name.resizeTextForBestFit = true; name.resizeTextMinSize = 26; name.resizeTextMaxSize = 45; if (deployed || !owned) { var tail = UiKit.Skin(UiKit.Rect(owned ? "Deployed" : "Locked", t, new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(10f, -96f), new Vector2(owned ? 84f : 104f, 38f)), "tail", owned ? UiKit.Col("tail_red", UiKit.Red) : UiKit.Col("tail_locked", UiKit.Dark)); UiKit.Title(UiKit.FillLabel(tail.transform, owned ? "출전" : "미보유", 24, UiKit.White), true); } // 병과 = 단색 심볼만(배경 틀 없음 · PD 2026-11-09 「심볼은 배경 프레임 없이 단색」) var role = UiKit.Rect("Role", t, new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), new Vector2(-42f, -42f), new Vector2(60f, 60f)); var sym = UiKit.Icon(role, UiKit.TypeSymbolId(h.type), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 60f); if (sym == null) UiKit.Title(UiKit.FillLabel(role, UnitTypeInfo.Mark(h.type), 28, UiKit.White), true); if (owned && s.CanGrowAny(h.id)) UiKit.Icon(t, "new_dot", new Vector2(0f, 0f), new Vector2(0.5f, 0.5f), new Vector2(64f, 70f), 30f); // 성장 가능 = 레벨 배지 오른쪽 위 return cell; } /// 원래 크기(320×431) 카드를 holder 가운데에 배율 k 로 넣는다(편성 자리 · 편성 보유 목록 · 영웅 목록 미보유 = owned false). public static Image CardIn(Transform holder, OutGameSession s, HeroDb.Hero h, bool deployed, System.Action onTap, float k, bool owned = true) { return Fit(Card(holder, s, h, deployed, onTap, owned), k); } /// 미보유·도감 안내 한 줄(이름(한자) · 병종 · ★ · 보유/미보유). public static string InfoText(HeroDb.Hero h, bool own) { return h.nameKo + (string.IsNullOrEmpty(h.hanja) ? "" : "(" + h.hanja + ")") + " · " + UnitTypeInfo.Korean(h.type) + " · ★" + h.rarity + (h.isSoldier ? " · 병사 유닛" : "") + (own ? " · 보유" : " · 미보유 — 거래 > 뽑기"); } public static Image EmptyIn(Transform holder, float k) { return Fit(Empty(holder), k); } static Image Fit(Image card, float k) { var rt = card.rectTransform; rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f); rt.pivot = new Vector2(0.5f, 0.5f); rt.sizeDelta = CellSize; rt.anchoredPosition = Vector2.zero; rt.localScale = new Vector3(k, k, 1f); return card; } /// 빈 카드(미보유 자리) = 어두운 바탕 + 문양 + 테두리 + 오른쪽 아래 얼굴 실루엣(팩 CardFrame01_Empty). public static Image Empty(Transform content) { var cell = UiKit.Cell(content, "Empty", Color.clear, null); cell.sprite = null; var t = cell.transform; // 팩 CardFrame01_Empty 그대로: Bg(#303146 · 마스크) 안에 Bg_d_Pattern 타일(#2D2F42) → 테두리 → 얼굴 var bg = UiKit.Layer(t, "Bg", "card_bg", UiKit.Col("card_empty", UiKit.Slot), 2f, 2f, 2f, 2f); bg.gameObject.AddComponent().showMaskGraphic = true; var pat = UiKit.Layer(bg.transform, "Pattern", "card_empty_pattern", UiKit.Col("card_empty_pattern", UiKit.Slot)); pat.type = Image.Type.Tiled; pat.pixelsPerUnitMultiplier = 1f; UiKit.Layer(t, "Border", "card_border", UiKit.Col("card_empty_border", UiKit.Line)); var face = UiKit.Icon(t, "face", new Vector2(1f, 0f), new Vector2(0.5f, 0.5f), new Vector2(-87f, 90.8f), 127f); if (face != null) face.color = UiKit.Col("card_empty_face", UiKit.Dark); return cell; } } /// /// 영웅 탭 = 영웅 목록(PD 2026-11-09 레퍼런스 = 팩 Collection_List) — 헤더(제목 · 장수/병사 고르기 ▼ · 사선 칸 「보유 / 전체」) · /// 세로 카드(보유 = 레벨 높은 순 → 탭 = 영웅 상세) + 미보유 = 그 영웅 카드(조금 어둡게 · 「미보유」 · 탭 = 획득 안내 · PD 2026-11-10). /// public class HeroListScreen : OutGameScreen { bool soldiers; RectTransform content; ScrollRect scroll; Text filterText; Text countText; int ownedCards; int lockedCards; UiAutoGrid autoGrid; public bool ShowingSoldiers { get { return soldiers; } } public override void Build() { Tab = 0; // 바탕 = 진남(레퍼런스 실측) + 재화 바 뒤 띠 UiKit.Paint(UiKit.Fill("ListBg", Rt), UiKit.Col("list_bg", UiKit.Bg), false); UiKit.Paint(UiKit.TopBand("ListTop", Rt, 0f, OutGameRoot.TopBarH), UiKit.Col("band_top", UiKit.Bg), false); var head = Header("영웅", "", true); // 오른쪽 사선 칸(보유 / 전체) — 팩 PanelFrame03_Oblique #D8E8FF 239 폭 var ob = UiKit.Rect("Oblique", head, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(239f, OutGameRoot.HeaderH - 7f)); UiKit.Skin(ob, "oblique", UiKit.Col("oblique", UiKit.White)); countText = UiKit.Title(UiKit.Label(ob, "", 52, UiKit.Col("count_text", UiKit.Dark), TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(12f, 0f), new Vector2(220f, 80f))); countText.horizontalOverflow = HorizontalWrapMode.Overflow; // 장수 / 병사 고르기(팩 「Property ▼」 자리 · 탭 = 전환) var pick = UiKit.Rect("Filter", head, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-252f, 0f), new Vector2(240f, 90f)); var hit = pick.gameObject.AddComponent(); hit.color = Color.clear; pick.gameObject.AddComponent