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