VSClone/Assets/Scripts/View/GameApp.Units.cs

449 lines
31 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 System.Text;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using VS.Sim;
// 유닛 메뉴(2026-10-29~30 PD) — 클래시 로얄 「전투 덱」처럼 위 = 덱(프리셋 1·2·3 · 6칸) · 아래 = 유닛 목록(등급 색 카드 · 참고 = 수집 카드 화면)
// 목록 = 유닛 그림 80종 전부 · 보유(전투 유닛) 먼저 · 나머지 = 미보유(회색 딤드 + 자물쇠 + 「미보유」)
// 목록 머리줄 오른쪽 끝 「편성」 = 편성 상태: 유닛을 누르거나 끌면 덱 칸에 화살표 → 덱 칸을 누르거나 그 칸에 놓으면 교체 · 편성 상태가 아니면 카드 = 유닛 상세
// 전투에는 열린 칸(전투 「강화」의 칸 열기)만 나감 · 등급 = battle.json grades(일반 → 고급 → 희귀 → 에픽 → 전설 → 불멸 → 신화 · 별 수 = 등급)
namespace VS.View
{
public partial class GameApp
{
public RectTransform UnitRoot;
RectTransform unitDeckGrid, unitCollGrid, unitLockGrid, unitDetail, unitSwapBar, unitDragGhost;
ScrollRect unitScroll;
Text unitDeckInfo, unitCollTitle, unitSwapText, unitEditLabel;
Button unitEditBtn;
readonly List<Button> presetTabs = new List<Button>();
readonly Dictionary<int, Image> collFrames = new Dictionary<int, Image>(); // 보유 유닛 카드 테 — 고른 유닛 = 노랑
bool unitEdit, lockBuilt; // 편성 상태 · 미보유 카드는 한 번만 만든다(바뀌지 않음)
int lockedCount;
int swapUnit; // 0 = 아님 · 아니면 덱 칸에 넣을 유닛 id(덱 칸에 화살표)
int detailUnit; // 상세에 띄운 유닛 id
Image udTag, udPortrait, udRole, udTraitIcon; Text udTagText, udName, udCost, udStatus, udTraits, udMainLabel; Button udMain, udPrev, udNext; GameObject udFly;
readonly List<Text> udStatValues = new List<Text>();
static readonly Color CardFrame = new Color32(28, 30, 52, 255);
const float CardW = 320f, CardH = 350f, CardGap = 24f;
void BuildUnitScreen()
{
UnitRoot = UI.Rect("Units", Root).Fill(0, TabH, 0, 0);
UI.Img(UnitRoot, "Bg", Art.Square, Skin.On ? Pal.Navy : (Color)new Color32(46, 40, 70, 255)).rectTransform.Fill();
if (Skin.Pattern != null) UI.Img(UnitRoot, "Pattern", Skin.Pattern, new Color(1f, 1f, 1f, 0.05f)).rectTransform.Fill();
// 목록(스크롤): 덱 머리줄 · 덱 6칸 · 평균 마나 · 목록 머리줄(+ 편성) · 보유 카드 · 미보유 카드
unitScroll = UI.Scroll(UnitRoot, "List", out var content, 14f);
((RectTransform)unitScroll.transform).Fill();
var head = UI.Rect("DeckHead", content); UI.SetLayoutHeight(head, 110);
UI.Txt(head, "T", "전투 덱", 54, Color.white, TextAnchor.MiddleLeft, true).rectTransform.Place(new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(24, 0), new Vector2(420, 90), new Vector2(0, 0.5f));
for (int i = 0; i < G.DeckPresets; i++)
{
int p = i;
var b = UI.TextBtn(head, "P" + i, (i + 1).ToString(), Pal.Gray, 44, () => SelectPreset(p), out _);
b.GetComponent<RectTransform>().Place(new Vector2(1, 0.5f), new Vector2(1, 0.5f), new Vector2(-24 - (G.DeckPresets - 1 - i) * 126, 0), new Vector2(110, 96), new Vector2(1, 0.5f));
presetTabs.Add(b);
}
unitDeckGrid = Grid(content, "DeckGrid");
UI.SetLayoutHeight(unitDeckGrid, GridH(T.Battle.slotMax));
unitDeckInfo = UI.Txt(content, "DeckInfo", "", 32, new Color32(214, 222, 255, 255), TextAnchor.MiddleCenter, true);
UI.SetLayoutHeight(unitDeckInfo, 64);
// 목록 머리줄 = 「유닛 목록 · 보유 N/M」 + 오른쪽 끝 「편성」(편성 상태 ↔ 완료)
var ch = UI.Rect("CollHead", content); UI.SetLayoutHeight(ch, 104);
unitCollTitle = UI.Txt(ch, "T", "", 46, Color.white, TextAnchor.MiddleLeft, true); unitCollTitle.supportRichText = true;
unitCollTitle.rectTransform.Fill(24, 0, 270, 0);
unitEditBtn = UI.TextBtn(ch, "Edit", "편성", Pal.Blue, 42, ToggleUnitEdit, out unitEditLabel);
unitEditBtn.GetComponent<RectTransform>().Place(new Vector2(1, 0.5f), new Vector2(1, 0.5f), new Vector2(-24, 0), new Vector2(220, 92), new Vector2(1, 0.5f));
unitCollGrid = Grid(content, "CollGrid");
unitLockGrid = Grid(content, "LockGrid");
// 편성 안내 띠(아래) — 고른 유닛 · 완료
var bar = UI.Img(UnitRoot, "SwapBar", Skin.Round, new Color(0.12f, 0.1f, 0.24f, 0.96f));
unitSwapBar = bar.rectTransform; unitSwapBar.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 16), new Vector2(-32, 130), new Vector2(0.5f, 0));
unitSwapText = UI.Txt(unitSwapBar, "T", "", 36, Pal.Yellow, TextAnchor.MiddleLeft, true); unitSwapText.rectTransform.Fill(30, 8, 260, 8);
var done = UI.TextBtn(unitSwapBar, "Done", "완료", Pal.Green, 40, ToggleUnitEdit, out _);
done.GetComponent<RectTransform>().Place(new Vector2(1, 0.5f), new Vector2(1, 0.5f), new Vector2(-24, 0), new Vector2(210, 100), new Vector2(1, 0.5f));
BuildUnitDetail();
UnitRoot.gameObject.SetActive(false);
}
RectTransform Grid(Transform parent, string name)
{
var g = UI.Rect(name, parent);
var gl = g.gameObject.AddComponent<GridLayoutGroup>();
gl.cellSize = new Vector2(CardW, CardH); gl.spacing = new Vector2(CardGap, CardGap); gl.childAlignment = TextAnchor.UpperCenter;
gl.constraint = GridLayoutGroup.Constraint.FixedColumnCount; gl.constraintCount = 3;
return g;
}
static float GridH(int n) { int rows = Mathf.Max(1, (n + 2) / 3); return rows * CardH + (rows - 1) * CardGap; }
static void ClearChildren(Transform t)
{
var old = new List<GameObject>();
foreach (Transform ch in t) old.Add(ch.gameObject);
foreach (var o in old) { o.SetActive(false); Kill(o); }
}
/// 유닛 탭 열기 — 편성 상태 · 상세 닫고 처음부터
public void OpenUnits()
{
unitEdit = false; swapUnit = 0;
if (unitDetail != null) unitDetail.gameObject.SetActive(false);
ShowUnits();
}
/// 덱 + 유닛 목록 다시 그리기
public void ShowUnits() { RefreshColl(); RefreshDeck(); }
/// 덱 칸 · 평균 마나 · 편성 버튼·띠 · 고른 유닛 카드 테(목록 카드는 다시 만들지 않음 — 끄는 중인 카드가 사라지지 않게)
void RefreshDeck()
{
var c = T.Battle; var deck = G.ActiveDeck;
for (int i = 0; i < presetTabs.Count; i++) UI.SetBtnColor(presetTabs[i], i == G.DeckSel ? Pal.Yellow : Pal.Gray);
ClearChildren(unitDeckGrid);
for (int i = 0; i < deck.Length; i++) DeckSlot(i, deck);
unitDeckInfo.text = $"평균 마나 {G.DeckAvgCost:0.0} · 열린 칸 {G.BattleSlots}/{c.slotMax} — 열린 칸 유닛만 전투에 나갑니다";
unitEditLabel.text = unitEdit ? "완료" : "편성";
UI.SetBtnColor(unitEditBtn, unitEdit ? Pal.Green : Pal.Blue);
unitSwapBar.gameObject.SetActive(unitEdit);
if (unitEdit) unitSwapText.text = swapUnit != 0 ? $"「{c.UnitDef(swapUnit)?.name}」 → 바꿀 덱 칸을 누르세요" : "편성 중 — 유닛을 누르거나 덱 칸으로 끌어 놓으세요";
foreach (var kv in collFrames) if (kv.Value != null) kv.Value.color = kv.Key == swapUnit ? Pal.Yellow : CardFrame;
}
void RefreshColl()
{
var c = T.Battle;
ClearChildren(unitCollGrid); collFrames.Clear();
foreach (var u in c.units) CollCard(u);
UI.SetLayoutHeight(unitCollGrid, GridH(c.units.Count));
if (!lockBuilt) BuildLockedCards();
unitCollTitle.text = $"유닛 목록 <size=34><color=#C8D2FF>보유 {c.units.Count}/{c.units.Count + lockedCount}</color></size>";
}
public void SelectPreset(int p) { G.SelectDeck(p); swapUnit = 0; ShowUnits(); }
public void ToggleUnitEdit() { unitEdit = !unitEdit; swapUnit = 0; ShowUnits(); }
/// 덱 칸 — 유닛 카드 / 빈 칸 · 잠긴 칸(강화 칸 열기 전) = 어둡게 + 자물쇠 · 편성 중 유닛을 고르면 = 노란 테 + 위에 화살표
void DeckSlot(int i, int[] deck)
{
var u = T.Battle.UnitDef(deck[i]);
int slot = i;
Button b;
if (u != null) b = UnitCard(unitDeckGrid, "Deck" + i, u, () => PickDeckSlot(slot));
else
{
b = UI.Btn(unitDeckGrid, "Deck" + i, Skin.Round, CardFrame, () => PickDeckSlot(slot));
UI.Img(b.transform, "In", Skin.RoundSoft, new Color(1f, 1f, 1f, 0.08f)).rectTransform.Fill(7, 7, 7, 7);
UI.Txt(b.transform, "T", "빈 칸", 42, new Color(1f, 1f, 1f, 0.6f), TextAnchor.MiddleCenter, true).rectTransform.Fill();
}
UI.Txt(b.transform, "No", (i + 1).ToString(), 30, new Color(1f, 1f, 1f, 0.85f), TextAnchor.MiddleCenter, true).rectTransform.Place(new Vector2(1, 1), new Vector2(1, 1), new Vector2(-26, -30), new Vector2(40, 40));
if (i >= G.BattleSlots)
{
UI.Img(b.transform, "Dim", Skin.RoundSoft, new Color(0.05f, 0.05f, 0.1f, 0.62f)).rectTransform.Fill(7, 7, 7, 7);
UI.Icon(b.transform, "Lock", Skin.Icon("lock"), Color.white).rectTransform.Place(new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, 24), new Vector2(84, 112));
UI.Txt(b.transform, "LT", "잠긴 칸", 30, Color.white, TextAnchor.MiddleCenter, true).rectTransform.Place(new Vector2(0, 0.5f), new Vector2(1, 0.5f), new Vector2(0, -62), new Vector2(0, 44));
}
if (swapUnit != 0)
{
b.GetComponent<Image>().color = Pal.Yellow;
var ar = UI.Icon(b.transform, "Arrow", Skin.S("Icon_Arrow_Down") ?? Skin.Icon("up"), Color.white);
ar.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -4), new Vector2(64, 56)); // 칸 위 가장자리 가운데(이웃 칸을 덜 가림)
ar.gameObject.AddComponent<Bob>();
}
}
/// 덱 칸 누름 — 고른 유닛이 있으면 그 칸에 넣기(교체) · 편성 중이면 안내 · 아니면 유닛 = 상세 · 잠긴 빈 칸 = 강화 · 열린 빈 칸 = 안내
public void PickDeckSlot(int slot)
{
if (swapUnit != 0) { G.DeckPut(slot, swapUnit); swapUnit = 0; ShowUnits(); return; }
if (unitEdit) { Toast("아래 목록에서 넣을 유닛을 먼저 고르세요"); return; }
var u = T.Battle.UnitDef(G.ActiveDeck[slot]);
if (u != null) OpenUnitDetail(u.id);
else if (slot >= G.BattleSlots) OpenBattleUpgrade();
else Toast("「편성」을 누르고 유닛을 골라 넣으세요");
}
void CollCard(BUnitDef u)
{
int id = u.id;
var b = UnitCard(unitCollGrid, "Unit" + id, u, () => TapUnitCard(id));
collFrames[id] = b.GetComponent<Image>();
if (unitEdit) { var dg = b.gameObject.AddComponent<UnitDrag>(); dg.app = this; dg.unitId = id; }
int pos = G.DeckPos(id);
if (pos < 0) return;
var tag = UI.Img(b.transform, "InDeck", Skin.S("Label_Parallelogram_Green") ?? Skin.Pill, Color.white);
tag.rectTransform.Place(new Vector2(1, 1), new Vector2(1, 1), new Vector2(-8, -10), new Vector2(126, 50), new Vector2(1, 1));
UI.Txt(tag.transform, "T", $"덱 {pos + 1}", 28, Color.white, TextAnchor.MiddleCenter, true).rectTransform.Fill(18, 2, 18, 2);
}
/// 보유 유닛 카드 누름 — 편성 중 = 고르기(덱 칸에 화살표 · 다시 누르면 취소) · 아니면 상세
public void TapUnitCard(int id)
{
if (!unitEdit) { swapUnit = 0; OpenUnitDetail(id); return; }
swapUnit = swapUnit == id ? 0 : id;
if (swapUnit != 0) unitScroll.verticalNormalizedPosition = 1f; // 덱이 보이게 맨 위로
RefreshDeck();
}
// ---------------- 끌어서 놓기(편성 중 보유 유닛 카드) ----------------
public void BeginUnitDrag(int id, PointerEventData e)
{
swapUnit = id;
var gh = UI.Icon(UnitRoot, "Ghost", UnitSprite(id), new Color(1f, 1f, 1f, 0.9f));
unitDragGhost = gh.rectTransform;
unitDragGhost.anchorMin = unitDragGhost.anchorMax = new Vector2(0.5f, 0.5f); unitDragGhost.sizeDelta = new Vector2(230, 230);
unitScroll.verticalNormalizedPosition = 1f;
RefreshDeck();
MoveUnitDrag(e);
}
public void MoveUnitDrag(PointerEventData e)
{
if (unitDragGhost != null && RectTransformUtility.ScreenPointToLocalPointInRectangle(UnitRoot, e.position, e.pressEventCamera, out var lp)) unitDragGhost.anchoredPosition = lp;
}
/// 놓은 곳이 덱 칸이면 그 칸으로 교체 · 아니면 고른 채로(화살표) — 칸을 누르면 교체
public void EndUnitDrag(PointerEventData e)
{
if (unitDragGhost != null) { Kill(unitDragGhost.gameObject); unitDragGhost = null; }
foreach (Transform ch in unitDeckGrid)
if (ch.gameObject.activeSelf && ch.name.StartsWith("Deck") && int.TryParse(ch.name.Substring(4), out var slot)
&& RectTransformUtility.RectangleContainsScreenPoint((RectTransform)ch, e.position, e.pressEventCamera)) { PickDeckSlot(slot); return; }
}
/// 미보유 = 그림 80종 중 보유 유닛이 쓰지 않는 것 — 회색 딤드 + 자물쇠 + 「미보유」 · 누르면 안내
void BuildLockedCards()
{
lockBuilt = true; lockedCount = 0;
var owned = new HashSet<int>();
foreach (var u in T.Battle.units) { int a = UnitArt.ArtOf(u.id); if (a > 0) owned.Add(a); }
foreach (var r in T.Battle.roster ?? new List<BRosterDef>())
{
if (owned.Contains(r.art)) continue;
lockedCount++;
var b = UI.Btn(unitLockGrid, "Lock" + r.art, Skin.Round, CardFrame, () => Toast("미보유 유닛 — 아직 편성할 수 없습니다"));
UI.Img(b.transform, "In", Skin.RoundSoft, new Color32(74, 76, 88, 255)).rectTransform.Fill(7, 7, 7, 7);
UI.Icon(b.transform, "Face", UnitArt.Get(r.art), new Color(0.58f, 0.58f, 0.62f)).rectTransform.Fill(36, 76, 36, 66);
UI.Img(b.transform, "Dim", Skin.RoundSoft, new Color(0.08f, 0.08f, 0.12f, 0.32f)).rectTransform.Fill(7, 7, 7, 7);
UI.Txt(b.transform, "Name", r.name, 34, new Color(0.82f, 0.82f, 0.86f), TextAnchor.MiddleLeft, true).rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(4, -12), new Vector2(-40, 52), new Vector2(0.5f, 1));
UI.Icon(b.transform, "Lock", Skin.Icon("lock"), Color.white).rectTransform.Place(new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, 26), new Vector2(84, 112));
UI.Txt(b.transform, "LT", "미보유", 36, Color.white, TextAnchor.MiddleCenter, true).rectTransform.Place(new Vector2(0, 0.5f), new Vector2(1, 0.5f), new Vector2(0, -62), new Vector2(0, 48));
}
UI.SetLayoutHeight(unitLockGrid, lockedCount > 0 ? GridH(lockedCount) : 0f);
}
/// 목록 스크롤 위치(1 = 맨 위 · 0 = 맨 아래)
public void ScrollUnits(float v) { Canvas.ForceUpdateCanvases(); unitScroll.verticalNormalizedPosition = v; Canvas.ForceUpdateCanvases(); }
/// 유닛 카드 — 짙은 테 · 안 = 등급 색(신화 = 무지개) · 왼쪽 위 이름 + 역할 · 가운데 그림 · 왼쪽 아래 마나 · 오른쪽 아래 별(= 등급)
Button UnitCard(Transform parent, string name, BUnitDef u, UnityAction click)
{
var b = UI.Btn(parent, name, Skin.Round, u.id == swapUnit ? Pal.Yellow : CardFrame, click);
var rt = (RectTransform)b.transform;
var mask = UI.Img(rt, "Mask", Skin.RoundSoft, Color.white); mask.rectTransform.Fill(7, 7, 7, 7);
mask.gameObject.AddComponent<Mask>().showMaskGraphic = false;
UI.Img(mask.transform, "Grade", GradeBg(u.grade), Color.white).rectTransform.Fill();
UI.Img(mask.transform, "Shade", Art.Square, new Color(0f, 0f, 0f, 0.26f)).rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), Vector2.zero, new Vector2(0, 78), new Vector2(0.5f, 0));
UI.Icon(rt, "Face", UnitSprite(u.id), Color.white).rectTransform.Fill(36, 76, 36, 66);
UI.Txt(rt, "Name", u.name, 36, Color.white, TextAnchor.MiddleLeft, true).rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(4, -12), new Vector2(-40, 52), new Vector2(0.5f, 1));
var role = UI.Img(rt, "Role", Skin.CirclePlain, new Color(0.1f, 0.1f, 0.18f, 0.7f));
role.rectTransform.Place(new Vector2(0, 1), new Vector2(0, 1), new Vector2(16, -66), new Vector2(54, 54), new Vector2(0, 1));
UI.Icon(role.transform, "I", RoleIcon(u), Color.white).rectTransform.Fill(9, 9, 9, 9);
var gem = UI.Icon(rt, "Gem", Skin.Gem ?? ManaDrop(), Color.white); gem.rectTransform.Place(new Vector2(0, 0), new Vector2(0, 0), new Vector2(48, 40), new Vector2(72, 66));
UI.Txt(gem.transform, "C", u.cost.ToString(), 36, Color.white, TextAnchor.MiddleCenter, true).rectTransform.Fill(0, 4, 0, 0);
var stars = UI.Rect("Stars", rt).Place(new Vector2(1, 0), new Vector2(1, 0), new Vector2(-14, 40), new Vector2(210, 34), new Vector2(1, 0.5f));
var hl = stars.gameObject.AddComponent<HorizontalLayoutGroup>();
hl.childAlignment = TextAnchor.MiddleRight; hl.spacing = -3; hl.childControlWidth = false; hl.childControlHeight = false; hl.childForceExpandWidth = false; hl.childForceExpandHeight = false;
for (int i = 0; i < Mathf.Max(1, u.grade); i++) UI.Icon(stars, "S" + i, Skin.Icon("star"), IsRainbow(u.grade) ? Color.HSVToRGB(i / 7f, 0.5f, 1f) : Color.white).rectTransform.sizeDelta = new Vector2(32, 32);
return b;
}
Sprite RoleIcon(BUnitDef u) => Skin.S(u.projectile ? "RoleIcon_Bow" : "RoleIcon_Sword") ?? Skin.Icon("pk");
bool IsRainbow(int grade) { var gd = T.Battle.Grade(grade); return gd != null && gd.color == "rainbow"; }
/// 등급 대표 색(battle.json) — 신화(무지개) = 분홍빛 보라
Color GradeColor(int grade)
{
var gd = T.Battle.Grade(grade);
if (gd == null) return Color.gray;
if (gd.color == "rainbow") return new Color32(224, 112, 255, 255);
return ColorUtility.TryParseHtmlString(gd.color, out var col) ? col : Color.gray;
}
/// 등급 바탕 — 위 밝게 → 아래 짙게 · 신화 = 대각선 무지개
Sprite GradeBg(int grade)
{
string key = "grade" + grade;
if (bcache.TryGetValue(key, out var s)) return s;
bool rainbow = IsRainbow(grade); Color baseC = GradeColor(grade);
const int W = 80, H = 100;
var r = new Raster(W, H);
for (int y = 0; y < H; y++)
for (int x = 0; x < W; x++)
{
float t = y / (float)(H - 1);
Color c = rainbow ? Color.HSVToRGB(Mathf.Repeat((x + (H - y)) / (float)(W + H), 1f), 0.42f, 1f) : Color.Lerp(baseC * 0.74f, Color.Lerp(baseC, Color.white, 0.22f), t);
c.a = 1f; r.Px[y * W + x] = c;
}
s = r.Sprite(); bcache[key] = s; return s;
}
static string RainbowText(string s)
{
var sb = new StringBuilder();
for (int i = 0; i < s.Length; i++) sb.Append($"<color=#{ColorUtility.ToHtmlStringRGB(Color.HSVToRGB(i / (float)Mathf.Max(1, s.Length) * 0.8f, 0.5f, 1f))}>{s[i]}</color>");
return sb.ToString();
}
// ---------------- 유닛 상세(참고 = 수집 카드 상세 화면) ----------------
void BuildUnitDetail()
{
unitDetail = UI.Rect("Detail", UnitRoot).Fill();
UI.Img(unitDetail, "Bg", Art.Square, new Color32(98, 110, 156, 255), true).rectTransform.Fill(); // 뒤 목록 누름 막기
if (Skin.Pattern != null) UI.Img(unitDetail, "Pattern", Skin.Pattern, new Color(1f, 1f, 1f, 0.07f)).rectTransform.Fill();
// 등급 띠 · 이름
udTag = UI.Img(unitDetail, "Grade", Skin.S("Label_Parallelogram_White_Bg") ?? Skin.Pill, Color.white);
udTag.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -64), new Vector2(230, 64));
udTagText = UI.Txt(udTag.transform, "T", "", 34, Color.white, TextAnchor.MiddleCenter, true); udTagText.supportRichText = true; udTagText.rectTransform.Fill(26, 4, 26, 4);
udName = UI.Txt(unitDetail, "Name", "", 84, Color.white, TextAnchor.MiddleCenter, true);
udName.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -152), new Vector2(900, 110));
// 그림 + 그림자 · 좌우 화살표 = 앞뒤 유닛
UI.Img(unitDetail, "Shadow", Art.SoftCircle, new Color(0f, 0f, 0f, 0.28f)).rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -652), new Vector2(480, 110));
udPortrait = UI.Icon(unitDetail, "Portrait", null, Color.white);
udPortrait.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -440), new Vector2(460, 460));
udPrev = ArrowBtn(unitDetail, "Prev", "Icon_Arrow_Prev1", "<", () => StepUnit(-1));
udPrev.GetComponent<RectTransform>().Place(new Vector2(0, 1), new Vector2(0, 1), new Vector2(84, -440), new Vector2(84, 164));
udNext = ArrowBtn(unitDetail, "Next", "Icon_Arrow_Next1", ">", () => StepUnit(1));
udNext.GetComponent<RectTransform>().Place(new Vector2(1, 1), new Vector2(1, 1), new Vector2(-84, -440), new Vector2(84, 164));
// 역할 · 마나 · 비행
var role = UI.Img(unitDetail, "Role", Skin.CirclePlain, new Color(0.1f, 0.1f, 0.18f, 0.75f));
role.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(-70, -724), new Vector2(72, 72));
udRole = UI.Icon(role.transform, "I", null, Color.white); udRole.rectTransform.Fill(12, 12, 12, 12);
var gem = UI.Icon(unitDetail, "Gem", Skin.Gem ?? ManaDrop(), Color.white);
gem.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(16, -724), new Vector2(80, 74));
udCost = UI.Txt(gem.transform, "C", "", 40, Color.white, TextAnchor.MiddleCenter, true); udCost.rectTransform.Fill(0, 4, 0, 0);
var fly = UI.Img(unitDetail, "Fly", Skin.S("Label_Parallelogram_Blue") ?? Skin.Pill, Color.white);
fly.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(140, -724), new Vector2(150, 60));
UI.Txt(fly.transform, "T", "비행", 32, Color.white, TextAnchor.MiddleCenter, true).rectTransform.Fill(22, 2, 22, 2);
udFly = fly.gameObject;
// 덱 상태(참고 화면의 레벨 막대 자리)
var st = UI.Img(unitDetail, "Status", Skin.Pill, new Color32(40, 44, 70, 235));
st.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -806), new Vector2(620, 64));
udStatus = UI.Txt(st.transform, "T", "", 34, Color.white, TextAnchor.MiddleCenter, true); udStatus.rectTransform.Fill(24, 4, 24, 4);
// 능력치 6칸(3줄 × 2)
string[] labels = { "체력", "공격력", "초당 피해", "공격 간격", "사거리", "이동 속도" };
Sprite[] icons = { Skin.Icon("heart"), Skin.Icon("pk"), Skin.Icon("fire"), Skin.Icon("afk"), Skin.S("RoleIcon_Bow") ?? Skin.Icon("pk"), Skin.S("RoleIcon_Boots") ?? Skin.Icon("auto") };
for (int i = 0; i < labels.Length; i++)
{
var cell = UI.Img(unitDetail, "Stat" + i, Skin.RoundSoft, new Color(1f, 1f, 1f, 0.16f));
cell.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(i % 2 == 0 ? -232 : 232, -906 - i / 2 * 128), new Vector2(440, 112));
UI.Icon(cell.transform, "I", icons[i], Color.white).rectTransform.Place(new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(56, 0), new Vector2(72, 72));
UI.Txt(cell.transform, "L", labels[i], 28, new Color32(225, 232, 255, 255), TextAnchor.MiddleLeft, true).rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(60, -6), new Vector2(-130, 36), new Vector2(0.5f, 1));
var v = UI.Txt(cell.transform, "V", "", 46, Color.white, TextAnchor.MiddleLeft, true);
v.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(60, 8), new Vector2(-130, 60), new Vector2(0.5f, 0));
udStatValues.Add(v);
}
// 특징(참고 화면의 Ability 자리) — battle.json 값에서 만든 설명
var ab = UI.Img(unitDetail, "Traits", Skin.RoundSoft, new Color(1f, 1f, 1f, 0.16f));
ab.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -1352), new Vector2(904, 214));
UI.Txt(ab.transform, "H", "특징", 36, Color.white, TextAnchor.MiddleCenter, true).rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -6), new Vector2(0, 46), new Vector2(0.5f, 1));
var tf = UI.Img(ab.transform, "IconBg", Skin.Round, new Color32(80, 190, 90, 255));
tf.rectTransform.Place(new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(30, -18), new Vector2(124, 124), new Vector2(0, 0.5f));
udTraitIcon = UI.Icon(tf.transform, "I", null, Color.white); udTraitIcon.rectTransform.Fill(20, 20, 20, 20);
udTraits = UI.Txt(ab.transform, "T", "", 30, Color.white, TextAnchor.UpperLeft, true);
udTraits.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 1), new Vector2(86, -26), new Vector2(-220, -70), new Vector2(0.5f, 0.5f));
// 덱에 넣기 / 덱에서 빼기 · 뒤로
udMain = UI.TextBtn(unitDetail, "Main", "", Pal.Green, 50, OnUnitMain, out udMainLabel);
udMain.GetComponent<RectTransform>().Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -1550), new Vector2(480, 150));
var back = UI.Btn(unitDetail, "Back", Skin.Dark, Color.white, CloseUnitDetail);
back.GetComponent<RectTransform>().Place(new Vector2(0, 0), new Vector2(0, 0), new Vector2(-20, 24), new Vector2(210, 128), new Vector2(0, 0));
UI.Icon(back.transform, "I", Skin.S("Icon_Back"), Color.white).rectTransform.Fill(56, 22, 44, 22);
unitDetail.gameObject.SetActive(false);
}
public void OpenUnitDetail(int id)
{
unitDetail.gameObject.SetActive(true);
ShowUnitDetail(id);
}
public void CloseUnitDetail()
{
unitDetail.gameObject.SetActive(false);
ShowUnits();
}
void StepUnit(int d)
{
var units = T.Battle.units; int i = units.FindIndex(u => u.id == detailUnit) + d;
if (i >= 0 && i < units.Count) ShowUnitDetail(units[i].id);
}
void ShowUnitDetail(int id)
{
var c = T.Battle; var u = c.UnitDef(id);
if (u == null) return;
detailUnit = id;
var gd = c.Grade(u.grade); bool rainbow = IsRainbow(u.grade);
udTag.color = rainbow ? (Color)new Color32(44, 40, 70, 255) : GradeColor(u.grade);
udTagText.text = gd == null ? "" : rainbow ? RainbowText(gd.name) : gd.name;
udName.text = u.name;
udPortrait.sprite = UnitSprite(u.id);
udRole.sprite = RoleIcon(u); udTraitIcon.sprite = RoleIcon(u);
udCost.text = u.cost.ToString();
udFly.SetActive(u.flying);
int pos = G.DeckPos(id);
udStatus.text = pos < 0 ? "덱에 없음" : $"덱 {G.DeckSel + 1} · {pos + 1}번 칸" + (pos >= G.BattleSlots ? " (잠긴 칸)" : "");
string[] vals = { $"{u.hp:0}", $"{u.damage:0}", $"{u.damage / Mathf.Max(0.01f, u.interval):0.#}", $"{u.interval:0.#}초", $"{u.range:0.#}칸", $"{u.speed:0.##}칸/초" };
for (int i = 0; i < vals.Length && i < udStatValues.Count; i++) udStatValues[i].text = vals[i];
var tr = new List<string> { u.projectile ? $"원거리 — {u.range:0.#}칸 밖에서 투사체를 쏩니다" : $"근접 — 붙어서 공격합니다(사거리 {u.range:0.#}칸)" };
if (u.flying) tr.Add("비행 — 강과 바위·언덕 위로 곧장 날아갑니다");
tr.Add($"마나 {u.cost} · 크기 {u.radius * 2f:0.#}칸");
udTraits.text = string.Join("\n", tr);
udMainLabel.text = pos >= 0 ? "덱에서 빼기" : "덱에 넣기";
UI.SetBtnColor(udMain, pos >= 0 ? Pal.Red : Pal.Green);
int idx = c.units.IndexOf(u);
udPrev.gameObject.SetActive(idx > 0); udNext.gameObject.SetActive(idx < c.units.Count - 1);
}
/// 덱에 넣기(빈 칸 → 바로 · 꽉 찼으면 편성 상태로 돌아가 바꿀 칸 고르기) / 덱에서 빼기
void OnUnitMain()
{
int id = detailUnit;
if (G.DeckPos(id) >= 0) { G.DeckRemove(id); ShowUnitDetail(id); return; }
if (G.DeckAdd(id)) { ShowUnitDetail(id); return; }
BeginUnitSwap(id);
}
public void BeginUnitSwap(int id)
{
unitEdit = true; swapUnit = id;
unitDetail.gameObject.SetActive(false);
ShowUnits();
unitScroll.verticalNormalizedPosition = 1f;
}
}
/// 편성 상태의 보유 유닛 카드 — 끌어서 덱 칸에 놓기(놓은 칸으로 교체)
public class UnitDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
public GameApp app; public int unitId;
public void OnBeginDrag(PointerEventData e) => app.BeginUnitDrag(unitId, e);
public void OnDrag(PointerEventData e) => app.MoveUnitDrag(e);
public void OnEndDrag(PointerEventData e) => app.EndUnitDrag(e);
}
/// 위아래로 통통 — 편성 중 덱 칸 화살표
public class Bob : MonoBehaviour
{
RectTransform rt; Vector2 home;
void Start() { rt = (RectTransform)transform; home = rt.anchoredPosition; }
void Update() { rt.anchoredPosition = home + new Vector2(0f, Mathf.Abs(Mathf.Sin(Time.unscaledTime * 6f)) * 12f); }
}
}