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

1012 lines
56 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;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 거래(img11 · 기획 S15) — ① 명장 등용(뽑기 · PD 2026-11-13: 배너 2쪽 = 픽업(영웅 1명 강조 일러 · 기간 · 상세 정보) / 국가별 상시(지도 위 진영 깃발 4 = 위·촉·오·군 · 주요 영웅 5) ·
/// 좌우 화살표로 쪽 넘김 · 천장 n/80 · 확률 공개 · 등용 1회/10회 · 첫 10회 무료 · 결과 = 삼고초려 연출 → 카드)
/// ② 보물 상자(PD #1093 C) = 상자 5종 · 보유 수 · 구매(chests.csv buy_gold/buy_gem/buy_points · 거래 할인 개발) · 열기는 보물 > 상자 탭
/// ③ 상점·패스 = 골드 승급 장비 상점(흰/녹 · 일일 · 새로고침 보옥 · 시장 개설 무료) · 성장 패스·월정액·챕터 팩·첫 결제 팩 = 「준비 중」 스텁(과금 없음).
/// </summary>
/// <summary>
/// 명장 등용 배너 배치 표(Resources/UI/recruit_banner.csv · 형식 = 로비 장면 표 LobbyScene.Load) — 지도·깃발·영웅 자리·이름표 좌표 = 배너 그림 px.
/// 누가 나오는지·이름·기간은 OutGame 표 gacha_banners.csv(GachaConfig.Banner).
/// </summary>
public static class RecruitBanner
{
public const string TablePath = "UI/recruit_banner";
static List<LobbyScene.Row> rows;
public static void Reload()
{
rows = null;
}
public static List<LobbyScene.Row> Rows
{
get
{
if (rows == null) rows = LobbyScene.Load(TablePath);
return rows;
}
}
public static LobbyScene.Row First(string kind)
{
return DomesticScene.First(Rows, kind);
}
/// <summary>인원 n 의 자리 줄(i_slot 순) — 그 인원 줄이 없으면 n 보다 큰 가장 작은 인원 줄.</summary>
public static List<LobbyScene.Row> Slots(string kind, int n)
{
int best = int.MaxValue;
foreach (var r in Rows)
if (r.kind == kind && r.n >= n && r.n < best) best = r.n;
var list = new List<LobbyScene.Row>();
foreach (var r in Rows)
if (r.kind == kind && r.n == best) list.Add(r);
list.Sort((a, b) => a.slot.CompareTo(b.slot));
return list;
}
}
public class TradeScreen : OutGameScreen
{
int page;
readonly List<Button> tabs = new List<Button>();
readonly List<RectTransform> pages = new List<RectTransform>();
Text pityText;
RectTransform pityBar;
GameObject freeChip;
GameObject freeChip2;
Button singleBtn;
Button tenBtn;
RectTransform pickupArt;
// 명장 등용 배너(PD 2026-11-13) — 쪽 = 픽업 배너 1개씩 + 상시 배너 묶음 1쪽(진영 깃발로 고름)
GachaConfig.Banner banner;
string standingId;
readonly List<List<GachaConfig.Banner>> bannerPages = new List<List<GachaConfig.Banner>>();
RectTransform bannerPx;
RectTransform bannerDots;
Text pityNote;
readonly Text[] probNote = new Text[4];
/// <summary>지금 고른 등용 배너.</summary>
public GachaConfig.Banner Banner
{
get { return banner; }
}
public int BannerPageCount
{
get { return bannerPages.Count; }
}
/// <summary>지금 배너가 있는 쪽(0부터).</summary>
public int BannerPage
{
get
{
for (int i = 0; i < bannerPages.Count; i++)
if (bannerPages[i].Contains(banner)) return i;
return 0;
}
}
/// <summary>배너에 세운 주요 영웅 수 · 진영 깃발 버튼 수(테스트·캡처).</summary>
public int FeaturedShown { get; private set; }
public int FlagCount { get; private set; }
RectTransform shopGrid;
ScrollRect shopScroll;
Text shopTimer;
Button refreshBtn;
GameObject resultRoot;
readonly List<GameObject> resultCards = new List<GameObject>();
Coroutine revealing;
GachaCutscene cutscene;
public List<GachaPull> LastPulls { get; private set; }
/// <summary>지금 재생 중인 뽑기 연출(삼고초려 장면 → 카드 획득 · 없으면 null).</summary>
public GachaCutscene Cutscene
{
get { return cutscene; }
}
public int Page
{
get { return page; }
}
public bool ResultVisible
{
get { return resultRoot != null && resultRoot.activeSelf; }
}
/// <summary>장비 상점 칸 수(스킬서 임시 판매 칸 제외).</summary>
public int ShopCardCount
{
get
{
int n = 0;
foreach (Transform c in shopGrid)
if (c.name != SkillBookCellName) n++;
return n;
}
}
public const string SkillBookCellName = "SkillBook";
/// <summary>스킬서 판매 칸이 있는가(PD #1093 B).</summary>
public bool SkillBookCellVisible
{
get { return shopGrid != null && shopGrid.Find(SkillBookCellName) != null; }
}
public override void Build()
{
Tab = 4;
if (UISkin.On) UiKit.Paint(UiKit.Fill("ListBg", Rt), UiKit.Col("list_bg", UiKit.Bg), false); // 목록형 바탕(Fable §5-3)
Header("거래", "등용 · 상자 · 상점 · 패스", true);
var body = Body();
string[] names = { "명장 등용", "보물 상자", "상점·패스" };
for (int i = 0; i < names.Length; i++)
{
int k = i;
tabs.Add(UISkin.On
? UiKit.SubTab(body, "Tab" + i, names[i], 24f + i * 348f, 18f, 332f, 84f, () => SetPage(k))
: UiKit.Btn("Tab" + i, body, names[i], 32, UiKit.Paper, UiKit.Ink, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f + i * 348f, -18f), new Vector2(332f, 84f), () => SetPage(k)));
pages.Add(UiKit.Fill("Page" + i, body, 0f, 0f, 118f, 0f));
}
BuildBannerPages();
banner = GachaRules.Default(S.Db, S.Now);
if (UISkin.Has("panel_field") && UISkin.Has("btn_l_yellow")) BuildGachaKit(pages[0]);
else BuildGacha(pages[0]);
BuildShop(pages[2]);
}
// ---------- 명장 등용(뽑기 · Fable 제안서 §4-4 · #1104 → PD 2026-11-13 배너 개편) ----------
const float GaugeW = 820f;
const float BannerW = 1032f, BannerH = 640f, BannerInset = 8f;
Text pityBadge;
Text tenCost;
Text singleCost;
void BuildBannerPages()
{
bannerPages.Clear();
List<GachaConfig.Banner> standing = null;
foreach (var b in S.Db.Gacha.banners)
{
if (b.IsPickup)
{
bannerPages.Add(new List<GachaConfig.Banner> { b });
continue;
}
if (standing == null)
{
standing = new List<GachaConfig.Banner>();
bannerPages.Add(standing);
}
standing.Add(b);
}
}
/// <summary>
/// 명장 등용 = 배너(틀 + 안쪽 그림 · 좌우 화살표 = 쪽 넘김 · 아래 쪽 점) → 천장 섹션(레벨형 게이지 + 노랑 배지 · 배너 묶음) →
/// 확률 섹션(제목 띠 · 행 3 = 별 아이콘 ×n + 바 + % + 배너별 메모) → 등용 1회(Secondary 하늘 + 보옥 비용) · 10회(Primary 노랑 + 「첫 무료」 꼬리 칩) · 각주 = ⓘ 팝업.
/// 배너 안쪽 내용은 고를 때마다 다시 그린다(DrawBanner · 좌표 = 표 recruit_banner.csv).
/// </summary>
void BuildGachaKit(RectTransform p)
{
var g = S.Db.Gacha;
// 배너 = 틀 + 안쪽(둥근 마스크 · 그림 px 좌표계)
var frame = UiKit.At(p, "Banner", 24f, 12f, BannerW, BannerH);
UiKit.Skin(frame, "banner", Color.white);
var clip = UiKit.Fill("Clip", frame, BannerInset, BannerInset, BannerInset, BannerInset);
UiKit.Skin(clip, "panel_field", Color.white);
clip.gameObject.AddComponent<Mask>().showMaskGraphic = false;
var view = RecruitBanner.First("view");
float aw = BannerW - 2f * BannerInset, ah = BannerH - 2f * BannerInset;
float vw = view != null && view.w > 1f ? view.w : aw, vh = view != null && view.h > 1f ? view.h : ah;
bannerPx = UiKit.At(clip, "Px", 0f, 0f, vw, vh);
bannerPx.localScale = new Vector3(aw / vw, aw / vw, 1f);
if (bannerPages.Count > 1)
{
BannerArrow(frame, "Prev", "arrow_prev", false);
BannerArrow(frame, "Next", "arrow_next", true);
}
bannerDots = UiKit.At(p, "Dots", 24f, 12f + BannerH + 3f, BannerW, 18f);
// 천장 게이지
RectTransform pr;
var pf = UiKit.SectionPanel(p, "Pity", 24f, 676f, 1032f, 176f, false, false, out pr);
UiKit.Title(UiKit.Label(pf, "천장", 34, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(26f, -12f), new Vector2(100f, 50f)), true);
pityNote = UiKit.Label(pf, "", 24, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(120f, -14f), new Vector2(300f, 46f), false);
pityText = UiKit.Label(pf, "", 28, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-26f, -12f), new Vector2(560f, 50f), false);
pityBar = UiKit.Gauge(pf, "Gauge", 20f, 62f, GaugeW + 60f, "lv_badge_y", "lv_fill", out pityBadge);
// 확률 공개
RectTransform probRoot;
var prob = UiKit.SectionPanel(p, "Prob", 24f, 864f, 1032f, 384f, false, false, out probRoot);
UiKit.SectionTitle(probRoot, "확률 공개");
var info = UiKit.Rect("Info", probRoot, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-16f, -12f), new Vector2(64f, 64f));
UiKit.Skin(info, "badge_circle", UiKit.Col("chip_dark", UiKit.Dark), true);
var ii = UiKit.Icon(info, "info", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 34f);
if (ii == null) UiKit.Title(UiKit.FillLabel(info, "i", 34, UiKit.White), true);
info.gameObject.AddComponent<Button>().onClick.AddListener(ShowGachaRules);
for (int t = 3; t >= 1; t--)
{
float y = 84f + (3 - t) * 94f;
var row = UiKit.Skin(UiKit.At(prob, "Row" + t, 20f, y, 1032f - 12f - 40f, 80f), "panel12", UiKit.Col("row_on_field", UiKit.Paper)).transform;
for (int s = 0; s < t; s++)
{
var st = UiKit.Icon(row, "star_on", new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(40f + s * 40f, 0f), 40f);
if (st != null) UiKit.DropShadow(st, 2f);
}
var fill = UiKit.Bar("P" + t, row, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(180f, 0f), new Vector2(460f, 30f), UiKit.Slot, t == 3 ? UiKit.Col("grade_orange_card_bg", UiKit.Coin) : t == 2 ? UiKit.Col("grade_purple_card_bg", UiKit.Purple) : UiKit.Col("grade_blue_card_bg", UiKit.Jade));
UiKit.SetBar(fill, g.P(t));
var pt = UiKit.Title(UiKit.Label(row, (g.P(t) * 100f).ToString("0.#") + "%", 32, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(660f, 0f), new Vector2(120f, 50f)), true);
pt.horizontalOverflow = HorizontalWrapMode.Overflow;
probNote[t] = UiKit.Label(row, "", 24, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-20f, 0f), new Vector2(200f, 40f), false);
probNote[t].horizontalOverflow = HorizontalWrapMode.Overflow;
}
// 등용 1회 · 10회
singleBtn = UiKit.SkinBtn(p, "Single", "btn_sky", UiKit.White, "1회 등용", 38, UiKit.White, 24f, 1276f, 420f, 116f, () => DoDraw(1)); // 버튼 행 = 탭 바 위 24 고정(검수 결함 4)
singleCost = UiKit.BtnCost(singleBtn, "cur_gem", "", 44f, 34);
tenBtn = UiKit.SkinBtn(p, "Ten", "btn_l_yellow", UiKit.White, g.TenCount + "회 등용", 44, UiKit.Col("text_on_yellow", UiKit.Ink), 1056f - 580f, 1270f, 580f, 122f, () => DoDraw(g.TenCount));
tenCost = UiKit.BtnCost(tenBtn, "cur_gem", "", 44f, 36);
var f2 = UiKit.Skin(UiKit.Rect("Free2", tenBtn.transform, new Vector2(1f, 1f), new Vector2(1f, 0.5f), new Vector2(-14f, 2f), new Vector2(200f, 50f)), "tail", UiKit.Col("tail_red", UiKit.Red));
UiKit.Title(UiKit.FillLabel(f2.transform, "첫 무료", 26, UiKit.White), true);
freeChip2 = f2.gameObject;
}
/// <summary>배너 좌우 화살표(쪽 넘김 · 틀 가장자리에 걸침 · 어두운 원 바탕 + 화살표).</summary>
void BannerArrow(Transform frame, string name, string icon, bool right)
{
var side = new Vector2(right ? 1f : 0f, 0.5f);
var hit = UiKit.Rect(name, frame, side, side, new Vector2(right ? -4f : 4f, 0f), new Vector2(68f, 124f));
hit.gameObject.AddComponent<Image>().color = Color.clear;
hit.gameObject.AddComponent<Button>().onClick.AddListener(() => StepBanner(right ? 1 : -1));
var c = new Vector2(0.5f, 0.5f);
UiKit.Skin(UiKit.Rect("Bg", hit, c, c, Vector2.zero, new Vector2(60f, 60f)), "badge_circle", UiKit.Col("chip_dark", UiKit.Dark));
var ic = UiKit.Icon(hit, icon, c, c, new Vector2(right ? 2f : -2f, 0f), 38f);
if (ic != null) ic.preserveAspect = true;
else UiKit.Title(UiKit.FillLabel(hit, right ? ">" : "<", 40, UiKit.White), true);
}
/// <summary>배너 고르기(진영 깃발 · 캡처·테스트) — 상시 배너면 그 진영을 기억한다(쪽을 넘겼다 돌아와도 유지).</summary>
public void SelectBanner(string id)
{
var b = S.Db.Gacha.Get(id);
if (b == null) return;
banner = b;
if (!b.IsPickup) standingId = b.id;
Refresh();
}
/// <summary>좌우 화살표 = 쪽 넘김(끝에서 처음으로 돈다).</summary>
public void StepBanner(int d)
{
int n = bannerPages.Count;
if (n == 0) return;
var page = bannerPages[((BannerPage + d) % n + n) % n];
var pick = page[0];
foreach (var b in page)
if (b.id == standingId) pick = b;
SelectBanner(pick.id);
}
/// <summary>「상세 정보」 = 이 배너 주요 영웅(픽업 영웅)의 풀 성장 스펙·스킬 팝업.</summary>
public void ShowBannerInfo()
{
if (banner != null && banner.featured.Count > 0) ShowHeroInfo(banner.featured[0]);
}
public bool ShowHeroInfo(string heroId)
{
return HeroInfoPopup.Show(Root, heroId);
}
// ---------- 배너 안쪽(고를 때마다 다시 그림 · 좌표 = 표 recruit_banner.csv 의 그림 px) ----------
static readonly Vector2 TL = new Vector2(0f, 1f);
void DrawBanner()
{
UiKit.Clear(bannerPx);
FeaturedShown = FlagCount = 0;
var bg = UiKit.Fill("ArtBg", bannerPx).gameObject.AddComponent<Image>();
bg.raycastTarget = false;
bg.sprite = UISkin.Icon(banner.art);
if (bg.sprite == null) bg.color = UiKit.Col("panel_field", UiKit.Dark);
if (banner.IsPickup) DrawPickup();
else DrawStanding();
UiKit.Clear(bannerDots);
if (bannerPages.Count < 2) return;
int at = BannerPage;
float dx = -((bannerPages.Count - 1) * 30f + 22f) / 2f;
for (int i = 0; i < bannerPages.Count; i++)
{
// 지금 쪽 = 긴 흰 알약 · 나머지 = 옅은 점(QA r1 M5 「쪽 점이 작고 흐림」)
float w = i == at ? 40f : 18f;
var dot = UiKit.Rect("Dot" + i, bannerDots, new Vector2(0.5f, 0.5f), new Vector2(0f, 0.5f), new Vector2(dx, 0f), new Vector2(w, 18f));
UiKit.Skin(dot, "chip", i == at ? UiKit.White : UiKit.Col("text_soft", UiKit.Disabled));
dx += w + 12f;
}
}
/// <summary>
/// 국가별 상시 배너(PD 2026-11-13 참고 1 + 추가 지시): 제목 줄 · 왼쪽 = 중국 대륙 지도 두루마리 위에 진영 깃발 4(마름모꼴 · ↖위 ↙촉 ↗군 ↘오 = 버튼) ·
/// 오른쪽 = 그 진영 주요 영웅 5명이 한 무리로 서고 저마다 세로 이름표(영웅을 누르면 상세 정보).
/// </summary>
void DrawStanding()
{
var px = bannerPx;
UiKit.InfoChip(px, "Kind", 22f, 20f, 150f, 50f, null, "상시 등용", 26);
var title = UiKit.Title(UiKit.Label(px, banner.nameKo, 54, UiKit.White, TextAnchor.MiddleLeft, TL, TL, new Vector2(188f, -8f), new Vector2(520f, 76f)), true);
title.horizontalOverflow = HorizontalWrapMode.Overflow;
var sub = UiKit.Title(UiKit.Label(px, banner.descKo, 24, UiKit.White, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -26f), new Vector2(430f, 40f)), true);
sub.horizontalOverflow = HorizontalWrapMode.Overflow;
var m = RecruitBanner.First("map");
if (m != null)
{
var map = UiKit.At(px, "Map", m.x, m.y, m.w, m.h);
var mi = map.gameObject.AddComponent<Image>();
mi.sprite = UISkin.Icon(m.sprite);
mi.raycastTarget = false;
mi.enabled = mi.sprite != null;
var glow = RecruitBanner.First("flag_glow");
var page = bannerPages[BannerPage];
// 고르지 않은 깃발 먼저 · 고른 깃발은 맨 위(크게 · 밑에 맥동하는 금빛)
for (int pass = 0; pass < 2; pass++)
foreach (var b in page)
{
bool on = b == banner;
if (on != (pass == 1)) continue;
var r = RecruitBanner.First("flag_" + b.faction);
if (r == null) continue;
var at = new Vector2(r.x, -r.y);
var c = new Vector2(0.5f, 0.5f);
if (on && glow != null && UISkin.Has(glow.sprite))
{
var gl = UiKit.Skin(UiKit.Rect("FlagGlow", map, TL, c, at, new Vector2(glow.w, glow.w)), glow.sprite, glow.color);
var pulse = gl.gameObject.AddComponent<UiPulse>();
pulse.min = glow.min;
pulse.max = glow.max;
pulse.period = glow.period;
}
float size = r.w * (on ? r.max : r.min);
var f = UiKit.Rect("Flag_" + b.id, map, TL, c, at, new Vector2(size, size));
var fi = f.gameObject.AddComponent<Image>();
fi.sprite = UISkin.Icon(r.sprite);
fi.preserveAspect = true;
float sh = on || r.shade < 0f ? 1f : r.shade;
fi.color = fi.sprite != null ? new Color(sh, sh, sh, 1f) : Color.clear;
if (fi.sprite != null) UiKit.DropShadow(fi, 4f);
else UiKit.FactionBadge(f, b.faction, c, c, Vector2.zero, size * 0.8f);
string id = b.id;
f.gameObject.AddComponent<Button>().onClick.AddListener(() => SelectBanner(id));
FlagCount++;
}
UiKit.InfoChip(px, "Hint", m.x + 30f, m.y + m.h + 10f, m.w - 60f, 46f, null, "깃발을 눌러 진영 선택", 24);
int heroes, soldiers;
CountPool(banner, out heroes, out soldiers);
UiKit.InfoChip(px, "Pool", m.x + 6f, m.y + m.h + 62f, m.w - 12f, 42f, null, UiKit.FactionKo(banner.faction) + " 진영 영웅 " + heroes + "명" + (soldiers > 0 ? " · 병사 " + soldiers + "종" : "") + " 등장", 22);
}
DrawFeatured();
}
/// <summary>이 배너에서 나오는 영웅 수 · 병사 유닛 종 수(풀 표 기준).</summary>
void CountPool(GachaConfig.Banner b, out int heroes, out int soldiers)
{
heroes = soldiers = 0;
for (int t = 1; t <= 3; t++)
foreach (var e in GachaRules.Candidates(S.Db, b, t))
{
var h = S.Def(e.heroId);
if (h != null && h.isSoldier) soldiers++;
else heroes++;
}
}
/// <summary>주요 영웅 무리(표 hero 줄 = 발 자리 · 뒤 → 앞 순서로 그림) + 세로 이름표(표 tag 줄) · 영웅 누름 = 상세 정보.</summary>
void DrawFeatured()
{
var px = bannerPx;
int n = banner.featured.Count;
var order = RecruitBanner.Slots("hero", n);
order.Sort((a, b) => a.y.CompareTo(b.y));
var hit = RecruitBanner.First("hit");
var shadow = RecruitBanner.First("shadow");
foreach (var r in order)
{
var h = r.slot < n ? S.Def(banner.featured[r.slot]) : null;
if (h == null) continue;
var foot = new Vector2(r.x, -r.y);
if (shadow != null && UISkin.Has(shadow.sprite))
UiKit.Skin(UiKit.Rect("Shadow", px, TL, new Vector2(0.5f, 0.5f), foot, new Vector2(r.h * shadow.w, r.h * shadow.h)), shadow.sprite, shadow.color);
if (UiKit.UnitStand("Hero_" + h.id, px, h, TL, foot, r.h, r.max) == null)
UiKit.Portrait("Hero_" + h.id, px, h, TL, new Vector2(0.5f, 0f), foot, new Vector2(r.h, r.h), false, 1f, Color.clear);
FeaturedShown++;
var hb = UiKit.Rect("Hit_" + h.id, px, TL, new Vector2(0.5f, 0f), foot, new Vector2(hit != null ? hit.w : r.h, hit != null ? hit.h : r.h));
hb.gameObject.AddComponent<Image>().color = Color.clear;
string id = h.id;
hb.gameObject.AddComponent<Button>().onClick.AddListener(() => ShowHeroInfo(id));
}
foreach (var r in RecruitBanner.Slots("tag", n))
{
var h = r.slot < n ? S.Def(banner.featured[r.slot]) : null;
if (h != null) NameTagV(px, r, h.nameKo);
}
}
/// <summary>
/// 세로 이름표(PD 참고 1 = 영웅마다 세로 이름표) — 표 tag 줄: 목패(가로 그림을 세워서 씀 · 9 분할 유지) · 판 색 · 글자 = 한 자씩 세로로.
/// f_x = 가운데 · f_y = 윗변(i_count 1 = 아랫변 · 머리 바로 위에 붙이는 이름표) · f_w 폭 · f_h 글자 1자 높이 · f_scale 글자 크기 · f_min 위아래 여백.
/// </summary>
static void NameTagV(Transform px, LobbyScene.Row r, string name)
{
float w = r.w > 1f ? r.w : 46f, ch = r.h > 1f ? r.h : 30f, pad = r.min > 1f ? r.min : 12f;
int fs = r.scale > 1f ? Mathf.RoundToInt(r.scale) : 26;
float h = name.Length * ch + pad * 2f;
var box = UiKit.Rect("Tag", px, TL, new Vector2(0.5f, r.count == 1 ? 0f : 1f), new Vector2(r.x, -r.y), new Vector2(w, h)); // i_count 1 = f_y 가 아랫변
var c = new Vector2(0.5f, 0.5f);
var plate = UiKit.Rect("Plate", box, c, c, Vector2.zero, new Vector2(h, w));
plate.localEulerAngles = new Vector3(0f, 0f, 90f);
var img = plate.gameObject.AddComponent<Image>();
img.sprite = UISkin.Icon(r.sprite);
img.color = r.color;
img.raycastTarget = false;
if (img.sprite != null && img.sprite.border.sqrMagnitude > 0f) img.type = Image.Type.Sliced;
for (int i = 0; i < name.Length; i++)
{
var t = UiKit.Title(UiKit.Label(box, name.Substring(i, 1), fs, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -(pad + i * ch)), new Vector2(w, ch)), true);
t.horizontalOverflow = HorizontalWrapMode.Overflow;
t.verticalOverflow = VerticalWrapMode.Overflow;
}
}
/// <summary>
/// 픽업 배너(PD 2026-11-13 참고 3 구성 = 왼쪽 정보 판 + 오른쪽 큰 일러스트 + 아래 이름표): 그림(영웅 일러 + 왼쪽 어둡게)은 한 장 ·
/// 왼쪽 = 종류 칩 → 제목(영웅 이름 강조) → ★ → 남은 기간 → 설명 → 인게임 그림 + 스킬 아이콘 → 천장 안내 · 오른쪽 아래 = 이름 명패 + 「상세 정보」.
/// </summary>
void DrawPickup()
{
var px = bannerPx;
var g = S.Db.Gacha;
var h = banner.featured.Count > 0 ? S.Def(banner.featured[0]) : null;
var now = S.Now;
bool open = GachaRules.IsOpen(banner, now);
var kind = UiKit.Skin(UiKit.At(px, "Kind", 74f, 24f, 176f, 52f), "chip", UiKit.Purple);
UiKit.Title(UiKit.FillLabel(kind.transform, "픽업 등용", 28, UiKit.White), true);
string title = banner.nameKo;
var names = new List<string>();
foreach (var id in banner.featured)
{
var fh = S.Def(id);
if (fh == null) continue;
names.Add(fh.nameKo);
title = title.Replace(fh.nameKo, "<color=#" + ColorUtility.ToHtmlStringRGB(UiKit.Star) + ">" + fh.nameKo + "</color>");
}
var tt = UiKit.Title(UiKit.Label(px, title, 68, UiKit.White, TextAnchor.MiddleLeft, TL, TL, new Vector2(74f, -82f), new Vector2(440f, 92f)), true);
tt.supportRichText = true;
tt.horizontalOverflow = HorizontalWrapMode.Overflow;
if (h != null)
for (int i = 0; i < h.rarity; i++)
UiKit.DropShadow(UiKit.Icon(px, "star_l", TL, TL, new Vector2(76f + i * 54f, -180f), 52f), 3f);
var clock = UiKit.Icon(px, "clock", TL, TL, new Vector2(76f, -246f), 40f);
string when = !open ? "등용 기간 종료" : GachaRules.EndOf(banner) == null ? "기간 제한 없음" : "남은 기간 " + RemainText(GachaRules.Remaining(banner, now));
var wt = UiKit.Title(UiKit.Label(px, when, 30, UiKit.White, TextAnchor.MiddleLeft, TL, TL, new Vector2(clock != null ? 126f : 76f, -245f), new Vector2(360f, 42f)), true);
wt.horizontalOverflow = HorizontalWrapMode.Overflow;
string desc = banner.descKo + "\n★★★ 등용 시 " + (g.PickupShare * 100f).ToString("0.#") + "% 확률로 " + string.Join(" · ", names.ToArray());
var dt = UiKit.Label(px, desc, 26, UiKit.White, TextAnchor.UpperLeft, TL, TL, new Vector2(76f, -302f), new Vector2(390f, 80f));
dt.lineSpacing = 1.15f;
UiKit.DropShadow(dt, 2f);
if (h != null)
{
// 인게임 그림 + 스킬 아이콘 2(액티브 · 패시브) — 누르면 상세 정보
const float y = 396f, cell = 108f;
var face = UiKit.Portrait("InGame", px, h, TL, TL, new Vector2(76f, -y), new Vector2(cell, cell), false, 1f, UiKit.Col("header", UiKit.Dark));
face.gameObject.AddComponent<Button>().onClick.AddListener(ShowBannerInfo);
Caption(px, "인게임", 76f, y + cell + 4f, cell);
string tk = UiKit.TypeKey(h.type);
for (int i = 0; i < 2; i++)
{
var sk = SkillRules.Skill(S.Db, h, i);
if (sk == null || UISkin.Icon("skill_" + sk.id) == null) continue;
float x = 76f + (i + 1) * (cell + 14f);
UiKit.Icon(px, "skill_" + sk.id, TL, TL, new Vector2(x + cell * 0.082f, -(y + cell * 0.082f)), cell * 0.836f);
if (tk != null) UiKit.DropShadow(UiKit.Icon(px, "skill_ring_" + tk, TL, TL, new Vector2(x, -y), cell), 3f);
Caption(px, i == 0 ? "액티브" : "패시브", x, y + cell + 4f, cell);
}
}
int left = Mathf.Max(1, g.Pity - GachaRules.Pity(S.Save, banner));
UiKit.InfoChip(px, "PityNote", 74f, 558f, 400f, 48f, null, "앞으로 " + left + "회 안에 ★★★ 확정", 26);
// 오른쪽 아래 = 이름 명패(병종 기호 · 이름 · ★) + 상세 정보
if (h != null)
{
var plate = UiKit.At(px, "Name", 500f, 530f, 348f, 82f);
var pimg = plate.gameObject.AddComponent<Image>();
pimg.raycastTarget = false;
pimg.sprite = UISkin.Icon("tre_plaque");
if (pimg.sprite != null)
{
if (pimg.sprite.border.sqrMagnitude > 0f) pimg.type = Image.Type.Sliced;
}
else
{
UISkin.Apply(pimg, "chip");
pimg.color = UiKit.Col("chip_dark", UiKit.Dark);
}
var row = UiKit.Fill("Row", plate, 62f, 62f, 6f, 12f);
var hl = row.gameObject.AddComponent<HorizontalLayoutGroup>();
hl.childAlignment = TextAnchor.MiddleCenter;
hl.spacing = 8f;
hl.childControlWidth = true;
hl.childControlHeight = false;
hl.childForceExpandWidth = hl.childForceExpandHeight = false;
var sym = UiKit.Icon(row, UiKit.TypeSymbolId(h.type), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 46f);
if (sym != null) Fixed(sym, 46f);
var nt = UiKit.Title(UiKit.Label(row, h.nameKo, 40, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(120f, 60f)), true);
nt.horizontalOverflow = HorizontalWrapMode.Overflow;
for (int i = 0; i < h.rarity; i++)
{
var st = UiKit.Icon(row, "star_on", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 30f);
if (st != null) Fixed(st, 26f);
}
UiKit.SkinBtn(px, "BannerInfo", "btn_sky", UiKit.White, "상세 정보", 28, UiKit.White, 856f, 530f, 150f, 80f, ShowBannerInfo);
}
}
static void Fixed(Graphic g, float w)
{
var le = g.gameObject.AddComponent<LayoutElement>();
le.minWidth = le.preferredWidth = w;
}
static void Caption(Transform px, string s, float x, float y, float w)
{
UiKit.DropShadow(UiKit.Label(px, s, 22, UiKit.Col("text_soft", UiKit.White), TextAnchor.MiddleCenter, TL, TL, new Vector2(x, -y), new Vector2(w, 30f), false), 2f);
}
/// <summary>남은 기간 글자(하루 이상 = n일 n시간 · 하루 미만 = n시간 n분).</summary>
public static string RemainText(TimeSpan r)
{
if (r.TotalDays >= 1.0) return (int)r.TotalDays + "일 " + r.Hours + "시간";
return (int)r.TotalHours + "시간 " + r.Minutes + "분";
}
/// <summary>같은 천장 묶음을 쓰는 배너 수.</summary>
int PityShare(GachaConfig.Banner b)
{
int n = 0;
foreach (var o in S.Db.Gacha.banners)
if (o.pityGroup == b.pityGroup) n++;
return n;
}
/// <summary>등용 규칙(구 각주 → ⓘ 팝업 · Fable §4-4) — 배너별 풀 · 천장 묶음 포함.</summary>
void ShowGachaRules()
{
var e = S.Db.Economy;
var g = S.Db.Gacha;
string scope = "";
if (banner != null)
{
if (banner.IsPickup)
{
var names = new List<string>();
foreach (var id in banner.featured)
if (S.Def(id) != null) names.Add(S.Def(id).nameKo);
scope = "픽업 배너 = ★★★ 의 " + (g.PickupShare * 100f).ToString("0.#") + "% 가 " + string.Join(" · ", names.ToArray()) + "\n";
}
else scope = "상시 배너 = " + UiKit.FactionKo(banner.faction) + " 진영 영웅만 등장\n";
if (g.PickupExclusive) scope += "픽업 영웅은 픽업 배너에서만 등장\n";
int share = PityShare(banner);
scope += "천장 " + g.Pity + "회 = ★★★ 확정 (" + (share > 1 ? "배너 " + share + "개가 같이 셈" : "이 배너만 셈") + ")\n\n";
}
Root.Modal("등용 규칙",
scope + "중복 = 영웅 조각\n★★★ " + e.ShardDup(3) + " · ★★ " + e.ShardDup(2) + " · ★ " + e.ShardDup(1) + "\n병사 중복 = 골드 " + e.SoldierDupGold +
"\n\n" + g.TenCount + "회 등용 = ★★ 이상 1 보장\n확률·천장 전부 공개",
new OutGameRoot.ModalButton("확인", UiKit.Dark, null));
}
public void SetPage(int p)
{
page = Mathf.Clamp(p, 0, pages.Count - 1);
Refresh();
}
public void OpenShop()
{
Root.Show(this);
SetPage(2);
}
public override void OnShow()
{
S.EnsureShop();
}
// ---------- 명장 등용(테마가 없을 때의 예전 화면) ----------
void BuildGacha(RectTransform p)
{
var g = S.Db.Gacha;
var bn = UiKit.TopBand("Banner", p, 12f, 440f, 24f, 24f);
if (UISkin.Has("banner")) UiKit.Skin(bn, "banner", Color.white); // 팩 배너 틀(PD #1100)
else UiKit.Paint(bn, new Color(0.27f, 0.14f, 0.12f));
var pick = UiKit.Box("Pickup", bn, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(30f, -24f), new Vector2(230f, 62f), UiKit.Red);
UiKit.FillLabel(pick.transform, "등용 배너", 30, UiKit.White);
UiKit.Title(UiKit.Label(bn, "명장 등용", 60, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(36f, -110f), new Vector2(600f, 90f)), true);
UiKit.Label(bn, "모든 영웅·병사 유닛\n= 등용으로 획득", 32, new Color(0.9f, 0.82f, 0.78f), TextAnchor.UpperLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(36f, -210f), new Vector2(520f, 110f), false);
var free = UiKit.Box("Free", bn, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(36f, 28f), new Vector2(300f, 78f), new Color(0.93f, 0.75f, 0.24f));
UiKit.FillLabel(free.transform, "첫 10회 무료", 34, UiKit.Ink);
freeChip = free.gameObject;
pickupArt = UiKit.Rect("PickupArt", bn, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-20f, 10f), new Vector2(440f, 400f));
var pity = UiKit.TopBand("Pity", p, 468f, 130f, 24f, 24f);
var pimg = UiKit.Paint(pity, UiKit.Paper);
UiKit.Border(pimg, UiKit.Line, 3f);
UiKit.Label(pity, "천장 게이지", 36, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -10f), new Vector2(400f, 56f));
pityText = UiKit.Label(pity, "", 30, UiKit.Red, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -10f), new Vector2(560f, 56f));
pityBar = UiKit.Bar("PityBar", pity, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 18f), new Vector2(980f, 32f), UiKit.Slot, UiKit.Red);
var prob = UiKit.TopBand("Prob", p, 614f, 270f, 24f, 24f);
var primg = UiKit.Paint(prob, UiKit.Paper);
UiKit.Border(primg, UiKit.Line, 3f);
UiKit.Label(prob, "확률 공개", 36, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -10f), new Vector2(400f, 56f));
UiKit.Label(prob, "확률·천장 전부 공개", 26, UiKit.InkSoft, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -10f), new Vector2(400f, 56f), false);
for (int t = 3; t >= 1; t--)
{
float y = -84f - (3 - t) * 58f;
UiKit.Label(prob, UiKit.Stars(t, t), 38, UiKit.Star, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, y), new Vector2(160f, 50f));
var fill = UiKit.Bar("P" + t, prob, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(200f, y - 12f), new Vector2(380f, 28f), UiKit.Slot, t == 3 ? UiKit.Red : new Color(0.47f, 0.38f, 0.32f));
UiKit.SetBar(fill, g.P(t));
UiKit.Label(prob, (g.P(t) * 100f).ToString("0.#") + " %" + (t == 1 ? " (병사 유닛 포함)" : ""), 32, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(600f, y), new Vector2(420f, 50f));
}
var btns = UiKit.TopBand("Buttons", p, 904f, 170f, 24f, 24f);
singleBtn = UiKit.Btn("Single", btns, "", 40, UiKit.Dark, UiKit.White, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), Vector2.zero, new Vector2(500f, 160f), () => DoDraw(1));
tenBtn = UiKit.Btn("Ten", btns, "", 40, UiKit.Red, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(500f, 160f), () => DoDraw(g.TenCount));
var f2 = UiKit.Box("Free2", tenBtn.transform, new Vector2(1f, 1f), new Vector2(1f, 0.5f), new Vector2(-10f, 0f), new Vector2(240f, 56f), new Color(0.93f, 0.75f, 0.24f));
UiKit.FillLabel(f2.transform, "첫 10회 무료", 26, UiKit.Ink);
freeChip2 = f2.gameObject;
var e = S.Db.Economy;
UiKit.Label(p, "중복 = 영웅 조각(★★★ " + e.ShardDup(3) + " · ★★ " + e.ShardDup(2) + " · ★ " + e.ShardDup(1) + ") · 병사 중복 = 골드 " + e.SoldierDupGold +
" · 10회 등용 = ★★ 이상 1 보장", 26, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -1090f), new Vector2(1020f, 80f), false);
}
void RefreshGacha()
{
var g = S.Db.Gacha;
var d = S.Save;
if (banner == null) return;
bool free = GachaRules.FirstTenFree(S.Db, d);
bool open = GachaRules.IsOpen(banner, S.Now);
int pity = GachaRules.Pity(d, banner);
if (freeChip != null) freeChip.SetActive(free);
freeChip2.SetActive(free);
singleBtn.interactable = tenBtn.interactable = open;
if (pityBadge != null)
{
pityText.text = pity + " / " + g.Pity + " · " + g.Pity + "회째 ★★★ 확정";
pityBadge.text = pity.ToString();
int share = PityShare(banner);
pityNote.text = share > 1 ? "배너 " + share + "개 공용" : "이 배너 전용";
UiKit.SetGauge(pityBar, pity / (float)Mathf.Max(1, g.Pity), GaugeW + 60f);
singleCost.text = g.CostSingle.ToString("N0");
tenCost.text = free ? "무료" : g.CostTen.ToString("N0");
var ic = tenCost.transform.parent.GetComponentInChildren<Image>(true);
if (ic != null) ic.gameObject.SetActive(!free);
// 확률 행 메모 = 배너별(★★★ = 픽업 영웅 몫 · ★ = 병사 포함)
var names = new List<string>();
if (banner.IsPickup)
foreach (var id in banner.featured)
if (S.Def(id) != null) names.Add(S.Def(id).nameKo);
probNote[3].text = names.Count > 0 ? string.Join("·", names.ToArray()) + " " + (g.PickupShare * 100f).ToString("0.#") + "%" : "";
probNote[2].text = "";
probNote[1].text = banner.soldiers ? "병사 포함" : "";
DrawBanner();
return;
}
pityText.text = pity + " / " + g.Pity + " · " + g.Pity + "회 ★★★ 확정";
UiKit.SetBar(pityBar, pity / (float)Mathf.Max(1, g.Pity));
UiKit.LabelOf(singleBtn).text = "1회 등용\n<size=32>보옥 " + g.CostSingle.ToString("N0") + "</size>";
UiKit.LabelOf(tenBtn).text = g.TenCount + "회 등용\n<size=32>" + (free ? "무료" : "보옥 " + g.CostTen.ToString("N0")) + "</size>";
UiKit.Clear(pickupArt);
for (int i = 0; i < banner.featured.Count && i < 2; i++)
{
var h = S.Def(banner.featured[i]);
if (h == null) continue;
UiKit.Portrait("Pick" + i, pickupArt, h, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-i * 210f, 50f), new Vector2(230f, 330f), false, 1f, new Color(1f, 1f, 1f, 0f));
UiKit.Label(pickupArt, h.nameKo + " ★" + h.rarity, 26, UiKit.White, TextAnchor.MiddleCenter, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-i * 210f, 6f), new Vector2(230f, 44f));
}
}
public void DoDraw(int count)
{
var why = GachaRules.CanDraw(S.Db, S.Save, banner, count, S.Now);
if (why != null)
{
Root.Toast(why);
return;
}
LastPulls = S.Draw(banner, count);
ShowResult(LastPulls);
}
// ---------- 뽑기 결과(카드 순차 공개) ----------
void ShowResult(List<GachaPull> pulls)
{
CloseResult();
// 삼고초려 연출(PD 2026-11-13 · 문이 덜컹 → 서서히 열리며 등급 빛 → 카드 획득 · 표 gacha_scene.csv + 배경 bg_gacha_hut 이 있을 때) · 없으면 아래 예전 결과 화면
if (pulls.Count > 0 && GachaScene.Ready)
{
cutscene = GachaCutscene.Show(Root, S, pulls, CloseResult);
resultRoot = cutscene.gameObject;
return;
}
var dim = UiKit.Fill("GachaResult", Root.TopRoot);
UiLayer.Nest(dim.gameObject, UiRate.Event, true); // 위 덮개 = 자기 캔버스(재화 바와 따로 · PD 2026-11-10)
var dimg = UiKit.Paint(dim, new Color(0.08f, 0.05f, 0.04f, 0.9f), false, true);
dimg.raycastTarget = true;
var skip = dim.gameObject.AddComponent<Button>();
skip.transition = Selectable.Transition.None;
skip.onClick.AddListener(RevealAll);
resultRoot = dim.gameObject;
UiKit.Label(dim, "등용 결과", 64, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -200f), new Vector2(900f, 100f));
int cols = Mathf.Min(5, Mathf.Max(1, pulls.Count));
int rows = (pulls.Count + cols - 1) / cols;
float cw = 196f, chh = 330f;
for (int i = 0; i < pulls.Count; i++)
{
int r = i / cols, c = i % cols;
int inRow = Mathf.Min(cols, pulls.Count - r * cols);
float x = (c - (inRow - 1) / 2f) * (cw + 12f);
float y = 120f - r * (chh + 20f) + (rows - 1) * (chh + 20f) / 2f;
resultCards.Add(ResultCard(dim, pulls[i], new Vector2(x, y), new Vector2(cw, chh)));
}
UiKit.Btn("Ok", dim, "확인", 40, UiKit.Red, UiKit.White, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 180f), new Vector2(420f, 110f), CloseResult);
revealing = Root.StartCoroutine(Reveal());
}
GameObject ResultCard(RectTransform parent, GachaPull p, Vector2 pos, Vector2 size)
{
var h = S.Def(p.heroId);
var back = UiKit.Box("Card", parent, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), pos, size, new Color(0.35f, 0.22f, 0.18f));
UiKit.Border(back, new Color(0.8f, 0.62f, 0.3f), 3f);
UiKit.FillLabel(back.transform, "?", 90, new Color(0.8f, 0.62f, 0.3f));
var front = UiKit.Fill("Front", back.transform);
UiKit.Paint(front, p.tier == 3 ? UiKit.Col("grade_orange_card_bg", UiKit.Coin) : p.tier == 2 ? UiKit.Purple : UiKit.Paper);
UiKit.Portrait("Face", front, h, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -10f), new Vector2(176f, 176f), false, 1.8f, new Color(0.9f, 0.86f, 0.8f));
UiKit.Label(front, UiKit.Stars(p.tier, p.tier), 30, UiKit.Star, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -188f), new Vector2(190f, 40f));
UiKit.Label(front, h != null ? h.nameKo : p.heroId, 32, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -226f), new Vector2(190f, 44f));
string tag = p.isNew ? "<color=#b0301e>NEW</color>" : p.shards > 0 ? "조각 +" + p.shards : p.gold > 0 ? "골드 +" + p.gold : "";
if (p.pity) tag += " 천장";
if (p.pickup) tag += " 픽업";
UiKit.Label(front, tag, 26, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 10f), new Vector2(190f, 44f));
front.gameObject.SetActive(false);
return front.gameObject;
}
IEnumerator Reveal()
{
foreach (var c in resultCards)
{
yield return new WaitForSecondsRealtime(0.14f);
if (c != null) c.SetActive(true);
}
revealing = null;
}
public void RevealAll()
{
if (cutscene != null)
{
cutscene.LandAll(); // 장면을 건너뛰고 카드 전부 공개
return;
}
if (revealing != null) Root.StopCoroutine(revealing);
revealing = null;
foreach (var c in resultCards)
if (c != null) c.SetActive(true);
}
public void CloseResult()
{
if (revealing != null) Root.StopCoroutine(revealing);
revealing = null;
resultCards.Clear();
cutscene = null;
if (resultRoot != null)
{
resultRoot.SetActive(false);
UnityEngine.Object.Destroy(resultRoot);
resultRoot = null;
}
Refresh();
}
public int RevealedCount
{
get
{
if (cutscene != null) return cutscene.CardsLanded;
int n = 0;
foreach (var c in resultCards)
if (c != null && c.activeSelf) n++;
return n;
}
}
// ---------- 장비 상점 ----------
void BuildShop(RectTransform p)
{
var head = UiKit.TopBand("ShopHead", p, 12f, 110f, 24f, 24f);
UiKit.Label(head, "승급 장비 상점", 44, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), Vector2.zero, new Vector2(400f, 70f));
var e = S.Db.Economy;
var grades = new List<string>();
foreach (var g in e.ShopGrades) grades.Add(UiKit.GradeKo(g));
UiKit.Label(head, "골드 · " + string.Join("/", grades.ToArray()) + " · 매일 갱신", 26, UiKit.InkSoft, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(330f, -4f), new Vector2(400f, 50f), false);
shopTimer = UiKit.Label(head, "", 28, UiKit.Red, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(320f, 50f));
shopGrid = UiKit.ScrollGrid("ShopGrid", p, 24f, 24f, 136f, 280f, new Vector2(326f, 400f), new Vector2(17f, 20f), 3, out shopScroll);
refreshBtn = UiKit.Btn("Refresh", p, "", 36, UiKit.Jade, UiKit.White, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 156f), new Vector2(640f, 96f), () =>
{
var why = S.RefreshShop();
Root.Toast(why ?? "상점 새로고침");
});
// 패스·팩 = 스텁(과금 없음 · PD #1093 C · 기획 S15)
string[] packs = { "성장 패스", "월정액", "챕터 팩", "첫 결제 팩" };
for (int i = 0; i < packs.Length; i++)
{
string name = packs[i];
UiKit.Btn("Pack" + i, p, name + "\n<size=22>준비 중</size>", 28, UiKit.Purple, UiKit.White, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(24f + i * 262f, 30f), new Vector2(246f, 108f),
() => Root.Modal(name, "준비 중입니다.\n(과금·패스 = 스텁 · 결제 없음 · 시스템 기획 S15)", new OutGameRoot.ModalButton("확인", UiKit.Dark, null)));
}
}
void RefreshShop()
{
S.EnsureShop();
var d = S.Save;
var left = EconomyRules.UntilNextDay(S.Now);
shopTimer.text = "갱신까지 " + ((int)left.TotalHours).ToString("00") + ":" + left.Minutes.ToString("00");
int free = EconomyRules.FreeRefreshesLeft(S.Db, d);
UiKit.LabelOf(refreshBtn).text = free > 0 ? "새로고침 · 무료 " + free + "회(시장 개설)" : "새로고침 · 보옥 " + S.Db.Economy.ShopRefreshGem;
UiKit.Clear(shopGrid);
for (int i = 0; i < d.shop.items.Count; i++)
{
var it = d.shop.items[i];
var g = S.Db.Gear.Get(it.gearId);
int k = i;
var cell = UiKit.Cell(shopGrid, "Item" + i, it.sold ? UiKit.Slot : UiKit.Paper, null);
UiKit.Border(cell, UiKit.Line, 2f);
var strip = UiKit.TopBand("Grade", cell.transform, 0f, 64f);
UiKit.Paint(strip, UiKit.GradeColor(g != null ? g.grade : ""));
UiKit.FillLabel(strip, (g != null ? UiKit.GradeKo(g.grade) : "?") + " 등급", 28, UiKit.White);
UiKit.Label(cell.transform, g != null ? g.nameKo : it.gearId, 40, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -86f), new Vector2(310f, 70f));
UiKit.Label(cell.transform, "보유 " + d.inv.GearCount(it.gearId) + " · 필요 영웅 " + NeedCount(it.gearId), 24, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -160f), new Vector2(310f, 44f), false);
UiKit.Label(cell.transform, "골드 " + it.price.ToString("N0"), 36, UiKit.Coin, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 120f), new Vector2(310f, 56f));
var b = UiKit.Btn("Buy", cell.transform, it.sold ? "품절" : "구매", 34, it.sold ? UiKit.Disabled : UiKit.Red, UiKit.White, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 20f), new Vector2(260f, 84f), () =>
{
var why = S.BuyShop(k);
Root.Toast(why ?? (g != null ? g.nameKo : "") + " 구매 → 가방");
});
b.interactable = !it.sold;
}
// 스킬서 임시 판매(B)는 제거 — 정식 획득처 = 사당 던전(PD #1093 C)
}
/// <summary>그 장비를 지금 등급 칸에서 기다리는(아직 안 채운) 보유 영웅 수.</summary>
int NeedCount(string gearId)
{
int n = 0;
foreach (var hs in S.Save.heroes)
{
var h = S.Def(hs.id);
var list = h != null ? S.Db.Heroes.GearFor(h, hs.grade) : null;
if (list == null) continue;
for (int i = 0; i < list.Count; i++)
if (list[i] == gearId && string.IsNullOrEmpty(hs.gear[i]))
{
n++;
break;
}
}
return n;
}
// ---------- 보물 상자(PD #1093 C) ----------
/// <summary>상자 구매 칸 수(테스트).</summary>
public int ChestRowCount { get; private set; }
void RefreshChests()
{
var p = pages[1];
UiKit.Clear(p);
var db = S.Db;
var d = S.Save;
UiKit.Label(p, "보물 상자 구매", 44, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(28f, -16f), new Vector2(500f, 70f));
UiKit.Label(p, "상자 포인트 " + d.inv.chestPoints + " · 열기 = 보물 > 상자", 28, UiKit.InkSoft, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-28f, -24f), new Vector2(560f, 54f), false);
ChestRowCount = 0;
for (int i = 0; i < db.Chests.chests.Count; i++)
{
var c = db.Chests.chests[i];
var row = UiKit.TopBand("Chest_" + c.id, p, 104f + i * 236f, 220f, 24f, 24f);
var img = UiKit.Paint(row, UiKit.Paper);
UiKit.Border(img, UiKit.Line, 3f);
var icon = UiKit.Box("Icon", row, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(20f, 0f), new Vector2(170f, 140f), TreasureScreen.ChestColor(c.id));
if (UiKit.Icon(icon.transform, "chest_" + c.id, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 8f), 120f) != null)
UiKit.Title(UiKit.Label(icon.transform, "×" + TreasureRules.ChestCount(d, c.id), 36, UiKit.White, TextAnchor.LowerRight, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-8f, 2f), new Vector2(150f, 50f)), true);
else UiKit.FillLabel(icon.transform, "×" + TreasureRules.ChestCount(d, c.id), 40, UiKit.White);
var probs = new List<string>();
for (int g = 0; g < ChestDb.ProbGrades.Length; g++)
if (c.p[g] > 0f) probs.Add(TreasureRules.GradeKo(ChestDb.ProbGrades[g]) + " " + (c.p[g] * 100f).ToString("0.#") + "%");
UiKit.Label(row, c.nameKo, 40, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(210f, -16f), new Vector2(420f, 60f));
UiKit.Label(row, string.Join(" · ", probs.ToArray()) + "\n포인트 +" + c.points + " · 획득처 " + c.source.Replace(";", "·"), 24, UiKit.InkSoft, TextAnchor.UpperLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(210f, -80f), new Vector2(440f, 120f), false);
int col = 0;
foreach (var cur in new[] { "gold", "gem", "points" })
{
int price = TreasureRules.Price(db, d, c, cur);
if (price <= 0) continue;
string id = c.id, currency = cur;
string label = (cur == "gold" ? "골드 " : cur == "gem" ? "보옥 " : "포인트 ") + price.ToString("N0");
var b = UiKit.Btn("Buy_" + cur, row, label, 28, cur == "gold" ? UiKit.Coin : cur == "gem" ? UiKit.Jade : UiKit.Grain, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-16f - col * 186f, 0f), new Vector2(176f, 96f), () =>
{
var why = S.Do(() => TreasureRules.Buy(S.Db, S.Save, id, currency));
Root.Toast(why ?? S.Db.Chests.Get(id).nameKo + " +1 (보물 > 상자에서 열기)");
});
col++;
}
if (col == 0) UiKit.Label(row, c.source.Contains("event") ? "이벤트(적장 토벌)" : "판매 안 함", 28, UiKit.InkSoft, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-24f, 0f), new Vector2(300f, 60f), false);
ChestRowCount++;
}
UiKit.Btn("ToTreasure", p, "보물 > 상자 열기", 34, UiKit.Dark, UiKit.White, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 24f), new Vector2(560f, 96f), () =>
{
Root.Show(Root.Treasures);
Root.Treasures.SetPage(2);
});
}
public override void Refresh()
{
for (int i = 0; i < tabs.Count; i++)
{
bool on = i == page;
if (UISkin.On) UiKit.SetSubTab(tabs[i], on);
else
{
tabs[i].GetComponent<Image>().color = on ? UiKit.Red : UiKit.Paper;
UiKit.LabelOf(tabs[i]).color = on ? UiKit.White : UiKit.Ink;
}
pages[i].gameObject.SetActive(on);
}
if (page == 0) RefreshGacha();
else if (page == 1) RefreshChests();
else if (page == 2) RefreshShop();
}
}
}