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

740 lines
40 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>영웅·병사 카드(2줄 · 기획 S12 v4) — 좌 초상(★ 는 초상 아래) · 1줄 = 병종 배지 · Lv · 출전 중 · 2줄 = 이름.</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, owned ? UiKit.Paper : new Color(0.86f, 0.84f, 0.8f), onTap);
UiKit.Border(cell, deployed ? UiKit.Red : UiKit.Line, deployed ? 4f : 2f);
var lv = s.Save.Level(h.id);
var hs = s.Save.Hero(h.id);
UiKit.Portrait("Face", cell.transform, h, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(12f, 8f), new Vector2(116f, 108f), false, 2f, new Color(0.9f, 0.86f, 0.8f));
int star = hs != null ? hs.star : h.rarity;
if (UISkin.Icon("star_on") != null) UiKit.StarRow(cell.transform, star, star, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(14f, 2f), 24f);
else UiKit.Label(cell.transform, UiKit.Stars(star, star), 22, UiKit.Star, TextAnchor.MiddleCenter, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(12f, 2f), new Vector2(116f, 30f));
UiKit.TypeBadge(cell.transform, h.type, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(142f, -16f), 40f);
UiKit.Title(UiKit.Label(cell.transform, owned ? "Lv " + (lv != null ? lv.lv : 1) : "미보유", 30, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(204f, -12f), new Vector2(150f, 48f)));
if (deployed)
{
var b = UiKit.Box("Deployed", cell.transform, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-12f, -14f), new Vector2(124f, 44f), UiKit.Red);
UiKit.Title(UiKit.FillLabel(b.transform, "출전 중", 24, UiKit.White), true);
}
if (hs != null)
{
var g = UiKit.Box("Grade", cell.transform, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-12f, 14f), new Vector2(56f, 48f), UiKit.GradeColor(hs.grade));
UiKit.GradeFrame(g, hs.grade);
UiKit.Title(UiKit.FillLabel(g.transform, UiKit.GradeKo(hs.grade), 24, UiKit.White), true);
}
var name = UiKit.Title(UiKit.Label(cell.transform, h.nameKo, 44, owned ? UiKit.Ink : UiKit.InkSoft, TextAnchor.MiddleLeft, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(142f, 12f), new Vector2(290f, 66f)));
name.horizontalOverflow = HorizontalWrapMode.Overflow;
if (owned && s.CanGrowAny(h.id) && UiKit.Icon(cell.transform, "new_dot", new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(14f, -10f), 34f) == null)
UiKit.Dot("New", cell.transform, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(14f, -10f), 26f, UiKit.Red);
return cell;
}
}
/// <summary>영웅 탭 = 보유 영웅·병사 목록(장수|병사 · 2줄 카드) → 탭 = 영웅 상세 · 도감.</summary>
public class HeroListScreen : OutGameScreen
{
bool soldiers;
RectTransform content;
ScrollRect scroll;
Button tabHero;
Button tabSoldier;
public bool ShowingSoldiers
{
get { return soldiers; }
}
public override void Build()
{
Tab = 0;
Header("영웅", "레벨 · 승급 · 진화 · 스킬", true);
var body = Body();
tabHero = UiKit.Btn("TabHero", body, "장수", 36, UiKit.Red, UiKit.White, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -20f), new Vector2(220f, 84f), () => SetFilter(false));
tabSoldier = UiKit.Btn("TabSoldier", body, "병사", 36, UiKit.Paper, UiKit.Ink, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(258f, -20f), new Vector2(220f, 84f), () => SetFilter(true));
UiKit.Btn("Codex", body, "도감", 36, UiKit.Dark, UiKit.White, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -20f), new Vector2(220f, 84f), () => HeroDetailScreen.ShowCodex(Root));
content = UiKit.ScrollGrid("List", body, 24f, 24f, 124f, 16f, new Vector2(506f, 150f), new Vector2(20f, 18f), 2, out scroll);
}
public void SetFilter(bool soldier)
{
soldiers = soldier;
Refresh();
}
public override void Refresh()
{
tabHero.GetComponent<Image>().color = soldiers ? UiKit.Paper : UiKit.Red;
UiKit.LabelOf(tabHero).color = soldiers ? UiKit.Ink : UiKit.White;
tabSoldier.GetComponent<Image>().color = soldiers ? UiKit.Red : UiKit.Paper;
UiKit.LabelOf(tabSoldier).color = soldiers ? UiKit.White : UiKit.Ink;
UiKit.LabelOf(tabHero).text = "장수 " + S.Owned(false).Count;
UiKit.LabelOf(tabSoldier).text = "병사 " + S.Owned(true).Count;
UiKit.Clear(content);
foreach (var h in S.Owned(soldiers))
{
var id = h.id;
HeroCards.Card(content, S, h, S.InActivePreset(id), true, () => Root.ShowHero(id));
}
}
public int CardCount
{
get { return content.childCount; }
}
}
/// <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;
var cell = 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 ? " · 보유" : " · 미보유 — 거래 > 뽑기")));
if (!own) cell.color = new Color(0.78f, 0.76f, 0.73f);
}
}
}
}