ThreeKingdomsSlingshot/Assets/Scripts/OutGame/UI/HeroScreens.cs

897 lines
51 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 영웅·병사 카드(가로형 · UI 상업용 재설계 ② · 설계서 §3② · 팩 CardFrame01 5층 등급색) — 초상(뒤 빛) · 병종 원 · Lv 배지 · 세력 깃발 · 출전 꼬리 칩 ·
/// 아래 이름 띠(이름 · ★ · 등급 칩) · 미보유 = 빈 카드(문양 + 초상 어둡게). 목록 · 편성 보유 행 · 도감 공용.
/// </summary>
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 배지 · 세력 깃발 · (출전 꼬리 칩)
var ring = UiKit.Skin(UiKit.Rect("Type", t, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(142f, -12f), new Vector2(52f, 52f)), "dot", UiKit.White);
ring.preserveAspect = true;
var cls = UiKit.Icon(ring.transform, UiKit.TypeIconId(h.type), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 36f);
if (cls == null) UiKit.Title(UiKit.FillLabel(ring.transform, 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;
}
var flag = UiKit.Icon(t, UISkin.Icon("flag_" + h.faction) != null ? "flag_" + h.faction : "flag_other", new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(264f, -14f), 44f);
UiKit.DropShadow(flag, 2f);
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;
}
}
/// <summary>
/// 영웅 목록 세로 카드(PD 2026-11-09 레퍼런스 = 팩 데모 Collection_List · 칸 320×431) — 등급색 바탕(마스크) · 그라데이션 · 일러 뒤 빛 · 일러 · 아래 어두운 띠 · 테두리 ·
/// 이름(왼쪽 위) · 병종 원 · 레벨 배지(왼쪽 아래 · 최고 = 노랑) · ★(오른쪽 아래) · 미보유 = 빈 카드(문양 + 얼굴 실루엣). 수치 = 팩 프리팹 RectTransform 그대로.
/// </summary>
public static class CollectionCards
{
public static readonly Vector2 CellSize = new Vector2(320f, 431f);
public static readonly Vector2 Spacing = new Vector2(22f, 20f);
public static Image Card(Transform content, OutGameSession s, HeroDb.Hero h, System.Action onTap)
{
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<Mask>().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)
{
// 우리 일러 = 전신 SD → 상반신이 카드를 채우게 키우고 아래로(다리는 아래 띠 뒤 · 레퍼런스 흉상 느낌)
var art = UiKit.Rect("Character", b, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -56f), new Vector2(470f, 460f)).gameObject.AddComponent<Image>();
art.sprite = sp;
art.preserveAspect = true;
art.raycastTarget = false;
}
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 = 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);
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;
}
// 이름(왼쪽 위 · 45) · 병종 원(#35374E 58×60)
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(24f, -40.5f), new Vector2(280f, 66f)), true);
name.horizontalOverflow = HorizontalWrapMode.Overflow;
var role = UiKit.Skin(UiKit.Rect("Role", t, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(46f, -100.9f), new Vector2(58f, 60f)), "dot", UiKit.Col("circle_inner", UiKit.Dark));
role.preserveAspect = true;
if (UiKit.Icon(role.transform, UiKit.TypeIconId(h.type), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 40f) == null)
UiKit.Title(UiKit.FillLabel(role.transform, UnitTypeInfo.Mark(h.type), 28, UiKit.White), true);
if (s.CanGrowAny(h.id)) UiKit.Icon(t, "new_dot", new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), new Vector2(-14f, -14f), 36f);
return cell;
}
/// <summary>빈 카드(미보유 자리) = 어두운 바탕 + 문양 + 테두리 + 오른쪽 아래 얼굴 실루엣(팩 CardFrame01_Empty).</summary>
public static Image Empty(Transform content)
{
var cell = UiKit.Cell(content, "Empty", Color.clear, null);
cell.sprite = null;
var t = cell.transform;
UiKit.Layer(t, "Bg", "card_bg", UiKit.Col("card_empty", UiKit.Slot), 2f, 2f, 2f, 2f);
var pat = UiKit.Layer(t, "Pattern", "card_empty_pattern", UiKit.Col("card_empty_pattern", UiKit.Slot), 8f, 8f, 8f, 8f);
pat.type = Image.Type.Tiled;
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;
}
}
/// <summary>
/// 영웅 탭 = 영웅 목록(PD 2026-11-09 레퍼런스 = 팩 Collection_List) — 헤더(제목 · 장수/병사 고르기 ▼ · 사선 칸 「보유 / 전체」) ·
/// 3열 세로 카드(보유 = 레벨 높은 순) + 미보유 자리 = 빈 카드 → 탭 = 영웅 상세.
/// </summary>
public class HeroListScreen : OutGameScreen
{
bool soldiers;
RectTransform content;
ScrollRect scroll;
Text filterText;
Text countText;
int ownedCards;
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<Image>();
hit.color = Color.clear;
pick.gameObject.AddComponent<Button>().onClick.AddListener(() => SetFilter(!soldiers));
filterText = UiKit.Title(UiKit.Label(pick, "", 38, UiKit.InkSoft, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-52f, 2f), new Vector2(180f, 70f)));
var arrow = UiKit.Icon(pick, "arrow_down", new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-6f, 0f), 38f);
UiKit.DropShadow(arrow, 3f);
var body = Body();
float side = (OutGameRoot.Reference.x - CollectionCards.CellSize.x * 3f - CollectionCards.Spacing.x * 2f) / 2f;
content = UiKit.ScrollGrid("List", body, side, side, 20f, 0f, CollectionCards.CellSize, CollectionCards.Spacing, 3, out scroll);
}
public void SetFilter(bool soldier)
{
soldiers = soldier;
Refresh();
}
public override void Refresh()
{
filterText.text = soldiers ? "병사" : "장수";
UiKit.Clear(content);
var owned = new List<HeroDb.Hero>(S.Owned(soldiers));
// 보유 = 레벨 높은 순 → ★ 높은 순
owned.Sort((a, b) =>
{
int la = S.Save.Level(a.id) != null ? S.Save.Level(a.id).lv : 1, lb = S.Save.Level(b.id) != null ? S.Save.Level(b.id).lv : 1;
if (la != lb) return lb.CompareTo(la);
var ha = S.Save.Hero(a.id);
var hb = S.Save.Hero(b.id);
return (hb != null ? hb.star : b.rarity).CompareTo(ha != null ? ha.star : a.rarity);
});
ownedCards = 0;
foreach (var h in owned)
{
var id = h.id;
CollectionCards.Card(content, S, h, () => Root.ShowHero(id));
ownedCards++;
}
int total = 0;
foreach (var h in S.Db.Heroes.heroes)
if (h.isSoldier == soldiers) total++;
for (int i = owned.Count; i < total; i++) CollectionCards.Empty(content);
countText.text = owned.Count + "/" + total;
}
/// <summary>보유 카드 수(빈 카드 제외).</summary>
public int CardCount
{
get { return ownedCards; }
}
}
/// <summary>
/// 영웅 상세(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;
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 Text[] slotName = new Text[6];
readonly Image[] slotCheck = new Image[6];
readonly Image[] slotBag = new Image[6];
Text lvBadge;
Text lvText;
Text expText;
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
{
get { return id; }
}
public void Select(string heroId)
{
id = heroId;
var h = S.Def(heroId);
order.Clear();
if (h != null)
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;
// ① 바탕(화면 전체 · 재화 바·헤더 뒤까지) = 등급 색 + 세로 글로우 + 문양 타일
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");
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;
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));
}
// ③ 레벨 행 = 배지 + 레벨 바 · 레벨업(녹 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++)
{
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);
}
// ⑥ 메뉴 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);
}
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);
}
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)
{
if (order.Count == 0) return;
int i = Mathf.Max(0, order.IndexOf(id));
id = order[(i + d + order.Count) % order.Count];
Refresh();
}
public override void Refresh()
{
var h = S.Def(id);
if (h == null) return;
var db = S.Db;
var lv = S.Save.Level(id);
var hs = S.Save.Hero(id);
var st = S.StatsOf(id);
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, Color.clear);
// 장비: 장착 = 체크 · 가방 보유 = 초록 화살표 · 없음 = 흐림
for (int i = 0; i < 6; i++)
{
string need = h.isSoldier ? null : HeroRules.RequiredGear(db, h, hs, i);
slotRoot[i].SetActive(need != null);
if (need == null) continue;
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);
}
// 레벨 · 승급 — 구조적으로 불가(최고 · 병사) = 회색 · 조건 부족 = 원래 색 + 누르면 사유 토스트
int lvCap = HeroRules.LevelCap(db, S.Save);
var row = db.Levels.Get(lv.lv);
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 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);
// 이름 띠
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++)
{
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)
{
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)
{
var h = S.Def(id);
var hs = S.Save.Hero(id);
if (h == null || hs == null) return;
var need = HeroRules.RequiredGear(S.Db, h, hs, i);
var g = S.Db.Gear.Get(need);
if (need == null) return;
if (!string.IsNullOrEmpty(hs.gear[i]))
{
Root.Toast("장착됨: " + (g != null ? g.nameKo : need) + " — 6칸을 모두 채우면 승급");
return;
}
var why = S.Equip(id, i);
if (why == null)
{
Root.Toast((g != null ? g.nameKo : need) + " 장착 · 1칸 = 1강(누적 " + hs.gearTotal + ")");
return;
}
string from = g != null ? g.dropFromChapter + "장~ 스테이지 드랍" : "";
Root.Modal("필요 장비", (g != null ? g.nameKo + " (" + UiKit.GradeKo(g.grade) + " 등급)" : need) + "\n보유 0\n\n획득: " + from + " · 거래 > 상점(흰/녹)" +
(g != null && g.craftCount > 0 ? "\n합성 = 하위 " + g.craftCount + "개 + 도면 " + g.craftBlueprint + " (2차)" : ""),
new OutGameRoot.ModalButton("상점 가기", UiKit.Red, () => Root.Trade.OpenShop()),
new OutGameRoot.ModalButton("닫기", UiKit.Dark, null));
}
void OnLevelUp()
{
var why = S.LevelUp(id);
Root.Toast(why ?? S.Def(id).nameKo + " Lv " + S.Save.Level(id).lv + "!");
}
void OnPromote()
{
var h = S.Def(id);
var why = S.Promote(id);
Root.Toast(why ?? h.nameKo + " 승급 → " + UiKit.GradeKo(S.Save.Hero(id).grade) + " 등급 · 새 장비 6칸");
}
/// <summary>스킬 레벨업(PD #1093 B): 액티브·패시브 중 고르기 → 스킬서 + 골드(skill_levels.csv) · 상한 = 등급 + ★3 액티브 +1.</summary>
public void OnSkillUp()
{
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;
var buttons = new List<OutGameRoot.ModalButton>();
var body = new System.Text.StringBuilder("보유 스킬서 " + S.Save.inv.books.skill + " · 골드 " + S.Save.gold.ToString("N0") + "\n");
for (int i = 0; i < 2; i++)
{
int k = i;
var sk = SkillRules.Skill(db, h, i);
if (sk == null) continue;
int lv = SkillRules.Level(hs, i);
var cost = db.Skills.CostTo(lv + 1);
var why = SkillRules.CanLevelUp(db, S.Save, h, hs, i);
body.Append("\n").Append(i == 0 ? "액티브 " : "패시브 ").Append(sk.nameKo).Append(" Lv ").Append(lv).Append(" / ").Append(SkillRules.Cap(db, h, hs, i));
if (cost != null && lv < sk.lvMax) body.Append(" → 스킬서 ").Append(cost.books).Append(" · 골드 ").Append(cost.gold.ToString("N0"));
if (why != null) body.Append("\n <color=#a03a22>").Append(why).Append("</color>");
buttons.Add(new OutGameRoot.ModalButton((i == 0 ? "액티브" : "패시브") + " Lv+1", why == null ? UiKit.Red : UiKit.Disabled, () =>
{
var r = SkillRules.LevelUp(S.Db, S.Save, h, hs, k);
if (r == null) S.Commit();
Refresh();
if (r == null) OnSkillUp(); // 이어서 올릴 수 있게 다시 연다
Root.Toast(r ?? sk.nameKo + " Lv " + SkillRules.Level(hs, k) + "!");
}));
}
buttons.Add(new OutGameRoot.ModalButton("닫기", UiKit.Dark, null));
Root.Modal("스킬 레벨업 — " + h.nameKo, body.ToString(), buttons.ToArray());
}
void OnEvolve()
{
var why = S.Evolve(id);
Root.Toast(why ?? S.Def(id).nameKo + " 진화 → ★" + S.Save.Hero(id).star);
}
/// <summary>정보 = 상세정보(스탯 내역) + 도감 버튼.</summary>
public void ShowStats()
{
var h = S.Def(id);
var st = S.StatsOf(id);
var e = S.Db.Economy;
var hs = S.Save.Hero(id);
string body =
"기본(" + UnitTypeInfo.Korean(h.type) + ") HP " + st.baseHp + " · 공 " + st.baseAtk + " · 방 " + st.baseDef + "\n" +
"영웅 배율 ×" + st.hpMult + " · ×" + st.atkMult + " · ×" + st.defMult + "\n" +
"레벨 Lv " + S.Save.Level(id).lv + " +" + st.lvPct.ToString("0.#") + "%\n" +
"승급 " + (hs != null ? UiKit.GradeKo(hs.grade) : "—") + " +" + st.promoPct.ToString("0.#") + "% · 장비 누적 " + (hs != null ? hs.gearTotal : 0) + "개 +" + st.gearPct.ToString("0.#") + "%\n" +
"진화 " + (hs != null ? "★" + hs.star : "—") + " +" + st.starPct.ToString("0.#") + "%\n" +
"훈련(" + TrainingRules.RankName(S.Db, TrainingRules.Rank(S.Save, h.type)) + ") HP +" + st.trainHpPct.ToString("0.#") + "% · 공 +" + st.trainPct.ToString("0.#") + "% · 방 +" + st.trainDefPct.ToString("0.#") + "%\n" +
"보물(수집품·세트) 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, () => ShowCodex(Root)),
new OutGameRoot.ModalButton("닫기", UiKit.Dark, null));
}
/// <summary>도감 = 전 영웅·병사(미보유 회색).</summary>
public static void ShowCodex(OutGameRoot root)
{
var s = root.Session;
int owned = 0;
foreach (var h in s.Db.Heroes.heroes)
if (s.Save.Owns(h.id)) owned++;
var content = root.ModalCustom("도감 " + owned + " / " + s.Db.Heroes.heroes.Count, 1560f, new OutGameRoot.ModalButton("닫기", UiKit.Dark, null));
ScrollRect sr;
var grid = UiKit.ScrollGrid("Codex", content, 0f, 0f, 0f, 0f, new Vector2(420f, 140f), new Vector2(20f, 14f), 2, out sr);
foreach (var h in s.Db.Heroes.heroes)
{
bool own = s.Save.Owns(h.id);
var hero = h;
HeroCards.Card(grid, s, h, false, own, () => root.Toast(hero.nameKo + (string.IsNullOrEmpty(hero.hanja) ? "" : "(" + hero.hanja + ")") + " · " +
UnitTypeInfo.Korean(hero.type) + " · ★" + hero.rarity + (hero.isSoldier ? " · 병사 유닛" : "") + (own ? " · 보유" : " · 미보유 — 거래 > 뽑기")));
}
}
}
}