1012 lines
56 KiB
C#
1012 lines
56 KiB
C#
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();
|
||
}
|
||
}
|
||
}
|