ThreeKingdomsSlingshot/Assets/Scripts/OutGame/Treasure/TreasureScreen.cs

564 lines
34 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 보물(하단 탭 · 기획 S8 · 목업 img07·16·17 · 카피바라 수집품 구조 · PD #1093 C) — 상단 탭 3:
/// ① 보유 중 = 그리드 4열 · 등급 색 · ★ · 미보유 회색 · 탭 = 상세(기본 효과 · 조건 효과 n/max · 강화 ★)
/// ② 세트 = 세트마다 구성 · 조각 게이지 · [세트]/[3성]/[6성]/[10성] 효과 ③ 상자 = 상자 포인트 게이지 → 명장 상자 · 선택 상자 + 확률 · 보유 수 · 열기(상자는 이 탭에서만 연다).
/// </summary>
public class TreasureScreen : OutGameScreen
{
public static readonly string[] PageNames = { "보유 중", "세트", "상자" };
int page;
int chestIdx;
bool chestPicked;
readonly List<Button> tabs = new List<Button>();
readonly List<RectTransform> pages = new List<RectTransform>();
public int Page
{
get { return page; }
}
public int GridCount { get; private set; }
public int SetCount { get; private set; }
public ChestResult LastOpen { get; private set; }
public string SelectedChest
{
get
{
var l = S.Db.Chests.chests;
return l.Count > 0 ? l[Mathf.Clamp(chestIdx, 0, l.Count - 1)].id : "";
}
}
public override void Build()
{
Tab = 1;
if (UISkin.On) UiKit.Paint(UiKit.Fill("ListBg", Rt), UiKit.Col("list_bg", UiKit.Bg), false); // 목록형 바탕(Fable §5-3)
Header("보물", "", true); // 부제 삭제(설계서 §2-5)
var body = Body();
for (int i = 0; i < PageNames.Length; i++)
{
int k = i;
tabs.Add(UISkin.On
? UiKit.SubTab(body, "Tab" + i, PageNames[i], 24f + i * 348f, 18f, 332f, 84f, () => SetPage(k))
: UiKit.Btn("Tab" + i, body, PageNames[i], 36, 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));
}
}
/// <summary>새 상자 화면 부품(Fable §4-5)이 테마에 있는가.</summary>
static bool Kit
{
get { return UISkin.Has("card3_bg") && UISkin.Has("banner_point") && UISkin.Has("btn_l_yellow"); }
}
/// <summary>보물 아이콘 id — 납품 그림(tre_&lt;id&gt;)이 있으면 그것 · 없으면 기본 스탯별 팩 임시 아이콘(tre_stat_* · Fable §6 N3 전까지).</summary>
static string IconId(TreasureDb.Item it)
{
if (UiKit.HasFinalIcon("tre_" + it.id)) return "tre_" + it.id;
return UISkin.Icon("tre_stat_" + it.baseStat) != null ? "tre_stat_" + it.baseStat : null;
}
public void SetPage(int p)
{
page = Mathf.Clamp(p, 0, pages.Count - 1);
Refresh();
}
public void SelectChest(string id)
{
int i = S.Db.Chests.IndexOf(id);
if (i >= 0) chestIdx = i;
chestPicked = true;
Refresh();
}
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);
}
UiKit.Clear(pages[page]);
if (page == 0) BuildOwned(pages[0]);
else if (page == 1) BuildSets(pages[1]);
else if (Kit) BuildChestsKit(pages[2]);
else BuildChests(pages[2]);
}
/// <summary>보물 등급 대표 색(테마 tre_&lt;등급&gt; · 희귀 청 · 영웅 자 · 전설 주 · 신화 적 · 명장 금).</summary>
public static Color GradeColor(string g)
{
return UiKit.Col("tre_" + g, UiKit.Slot);
}
/// <summary>
/// 보물 칸(UI 상업용 재설계 ⑥ · 설계서 §3⑥) = 팩 ItemFrame 5층 등급색 + 아이콘(납품 tre_* · 없으면 이름 첫 글자) + ★ 또는 조각 바 + 아래 이름 판 ·
/// 미보유 = 진청 빈 칸 + 흰 25 % 실루엣.
/// </summary>
void TreasureCell(Transform parent, TreasureDb.Item it, bool showShards)
{
var st = S.Save.inv.Treasure(it.id);
bool owned = st != null;
var cell = UiKit.Cell(parent, "T_" + it.id, Color.clear, owned ? (System.Action)(() => Detail(it.id)) : null);
cell.sprite = null; // 바닥 = 클릭 판(투명)
var t = cell.transform;
UiKit.ItemSlot(t, owned ? "tre_" + it.grade + "_" : "tre_none_", owned);
var art = UiKit.Rect("Art", t, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -100f), new Vector2(140f, 140f));
string icon = IconId(it);
if (icon != null)
{
// 아이콘 = 납품 그림 또는 기본 스탯별 임시(Fable §4-5 「글자 → 보물 아이콘」) · 미보유 = 흰 25 % 실루엣
var ic = UiKit.Icon(art, icon, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, icon.StartsWith("tre_stat_") ? 120f : 140f);
if (owned) UiKit.DropShadow(ic, 5f);
else ic.color = UiKit.Col("tre_none_icon", UiKit.InkSoft);
}
else if (owned) UiKit.Title(UiKit.FillLabel(art, it.nameKo.Substring(0, 1), 84, UiKit.White), true);
if (owned)
{
if (showShards && st.star < it.starMax)
{
int need, iron;
TreasureRules.EnhanceCost(S.Db, it, st.star + 1, out need, out iron);
var fill = UiKit.Bar("Shards", t, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 76f), new Vector2(150f, 26f), UiKit.Slot, GradeColor(it.grade));
UiKit.SetBar(fill, st.shards / (float)Mathf.Max(1, need));
UiKit.FillLabel(fill.parent, st.shards + "/" + need, 18, UiKit.White);
}
else if (st.star > 0) UiKit.StarRow(t, st.star, st.star, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 72f), 30f);
}
// 이름 띠 = 칸 아래에 붙은 카드 띠(CardFrame01_Bottom · Fable §4-5) · 테마가 없으면 둥근 판
var band = UISkin.Has("card_bottom")
? UiKit.Layer(t, "Name", "card_bottom", UiKit.Col("card_bottom", UiKit.Dark), 6f, 0f, 6f, 6f)
: UiKit.Skin(UiKit.Rect("Name", t, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 12f), new Vector2(206f, 52f)), "plate12", UiKit.Col("gear_plate", UiKit.Dark));
if (UISkin.Has("card_bottom"))
{
band.rectTransform.anchorMax = new Vector2(1f, 0f);
band.rectTransform.offsetMax = new Vector2(-6f, 62f);
}
var nt = UiKit.FillLabel(band.transform, owned ? it.nameKo : "미보유", 26, owned ? UiKit.White : UiKit.Col("text_soft", UiKit.InkSoft));
nt.horizontalOverflow = HorizontalWrapMode.Overflow;
}
// ---------- ① 보유 중 ----------
void BuildOwned(RectTransform p)
{
var db = S.Db;
int owned = 0;
foreach (var it in db.Treasures.items)
if (S.Save.inv.Treasure(it.id) != null) owned++;
var cnt = UiKit.Title(UiKit.Label(p, "보유 " + owned + " / " + db.Treasures.items.Count, 36, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(28f, -8f), new Vector2(360f, 56f)), UISkin.On);
cnt.horizontalOverflow = HorizontalWrapMode.Overflow;
var grades = db.Economy.TreasureGrades;
for (int i = 0; i < grades.Count; i++)
{
// 등급 범례 칩 = 서브탭 틀 소형(Round10 · Fable §4-5 「120×56 통일」) · 등급 대표 색
var pos = new Vector2(-24f - (grades.Count - 1 - i) * 126f, -8f);
var chip = UISkin.Has("subtab")
? UiKit.Skin(UiKit.Rect("G" + i, p, new Vector2(1f, 1f), new Vector2(1f, 1f), pos, new Vector2(118f, 56f)), "subtab", GradeColor(grades[i].Key))
: UiKit.Box("G" + i, p, new Vector2(1f, 1f), new Vector2(1f, 1f), pos, new Vector2(110f, 48f), GradeColor(grades[i].Key));
UiKit.Title(UiKit.FillLabel(chip.transform, TreasureRules.GradeKo(grades[i].Key), 26, UiKit.White), UISkin.On);
}
ScrollRect sr;
var grid = UiKit.ScrollGrid("Grid", p, 24f, 24f, 76f, 64f, new Vector2(240f, 290f), new Vector2(16f, 16f), 4, out sr);
// 정렬 = 보유 먼저 · 등급 높은 순
var list = new List<TreasureDb.Item>(db.Treasures.items);
list.Sort((a, b) =>
{
bool oa = S.Save.inv.Treasure(a.id) != null, ob = S.Save.inv.Treasure(b.id) != null;
if (oa != ob) return oa ? -1 : 1;
return db.Economy.TreasureGradeOrder(b.grade).CompareTo(db.Economy.TreasureGradeOrder(a.grade));
});
GridCount = 0;
foreach (var it in list)
{
TreasureCell(grid, it, false);
GridCount++;
}
var bonus = new List<string>();
foreach (var s in TreasureDb.BaseStats)
{
float v = TreasureRules.StatPct(db, S.Save, s);
if (v > 0f) bonus.Add(TreasureRules.StatKo(s) + " +" + v.ToString("0.#") + "%");
}
string fx = bonus.Count > 0 ? "전체 효과 " + string.Join(" · ", bonus.ToArray()) : "보물을 모으면 전 유닛에 상시 효과";
if (UISkin.Has("chip"))
{
// 아래 효과 줄 = 정보 칩(§5-6)
var c =UiKit.Skin(UiKit.Rect("Effects", p, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 4f), new Vector2(1032f, 54f)), "chip", UiKit.Col("chip_dark", UiKit.Dark));
var ft = UiKit.FillLabel(c.transform, fx, 26, UiKit.White, TextAnchor.MiddleCenter, false, 20f);
ft.horizontalOverflow = HorizontalWrapMode.Overflow;
}
else UiKit.Label(p, fx, 26, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 8f), new Vector2(1030f, 50f), false);
}
public void Detail(string id)
{
var db = S.Db;
var d = S.Save;
var it = db.Treasures.Get(id);
var st = d.inv.Treasure(id);
if (it == null || st == null) return;
var sb = new System.Text.StringBuilder();
sb.Append(TreasureRules.GradeKo(it.grade)).Append(" · ★").Append(st.star).Append("/").Append(it.starMax);
var tiers = it.setId.Length > 0 ? db.TreasureSets.TiersOf(it.setId) : null;
if (tiers != null && tiers.Count > 0) sb.Append(" · 세트 「").Append(tiers[0].nameKo).Append("」");
sb.Append("\n\n<b>기본 효과</b> ").Append(TreasureRules.StatKo(it.baseStat)).Append(" +").Append(TreasureRules.BasePct(db, it, st.star).ToString("0.##")).Append("%");
int stacks = TreasureRules.CondStacks(d, it);
sb.Append("\n<b>조건 효과</b> ").Append(TreasureRules.CondKo(it.condKey)).Append(" ").Append(it.condEvery).Append("회마다 +").Append(it.condPct.ToString("0.##")).Append("% (")
.Append(stacks).Append("/").Append(it.condMax).Append(" · 누적 ").Append(TreasureRules.Counter(d, it.condKey)).Append(")");
sb.Append("\n<size=28>= 지금 ").Append(TreasureRules.StatKo(it.baseStat)).Append(" +").Append(TreasureRules.Value(db, d, it).ToString("0.##")).Append("%</size>");
string btn;
var why = TreasureRules.CanEnhance(db, d, id);
if (st.star < it.starMax)
{
int shards, iron;
TreasureRules.EnhanceCost(db, it, st.star + 1, out shards, out iron);
sb.Append("\n\n<b>강화 ★").Append(st.star + 1).Append("</b> 조각 ").Append(st.shards).Append("/").Append(shards).Append(" · 운철 ").Append(d.inv.iron).Append("/").Append(iron);
btn = why == null ? "강화" : why;
}
else btn = "최고 ★";
Root.Modal(it.nameKo, sb.ToString(),
new OutGameRoot.ModalButton(btn, why == null ? UiKit.Red : UiKit.Disabled, () =>
{
var w = S.Do(() => TreasureRules.Enhance(S.Db, S.Save, id));
if (w == null) Detail(id);
else Root.Toast(w);
}, true),
new OutGameRoot.ModalButton("닫기", UiKit.Dark, null));
}
// ---------- ② 세트 ----------
void BuildSets(RectTransform p)
{
var db = S.Db;
var d = S.Save;
ScrollRect sr;
var list = UiKit.ScrollGrid("Sets", p, 24f, 24f, 6f, 12f, new Vector2(1032f, 640f), new Vector2(0f, 24f), 1, out sr);
SetCount = 0;
foreach (var setId in db.TreasureSets.SetIds)
{
var tiers = db.TreasureSets.TiersOf(setId);
if (tiers.Count == 0) continue;
var head = tiers[0];
var block = UiKit.Cell(list, "Set_" + setId, new Color(0f, 0f, 0f, 0f), null);
block.raycastTarget = false;
var band = UiKit.Box("Band", block.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), Vector2.zero, new Vector2(700f, 70f), UiKit.Dark);
UiKit.FillLabel(band.transform, head.nameKo, 40, UiKit.White);
int owned = TreasureRules.OwnedIn(d, head);
UiKit.Label(block.transform, "보유 " + owned + "/" + head.members.Count, 30, owned == head.members.Count ? UiKit.Ink : UiKit.Red, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(0f, -10f), new Vector2(200f, 50f));
var row = UiKit.Rect("Members", block.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -84f), new Vector2(1032f, 290f));
var hl = row.gameObject.AddComponent<HorizontalLayoutGroup>();
hl.spacing = 12f;
hl.childAlignment = TextAnchor.UpperCenter;
hl.childControlWidth = hl.childControlHeight = false;
foreach (var m in head.members)
{
var it = db.Treasures.Get(m);
if (it == null) continue;
var holder = UiKit.Rect("M", row, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(196f, 290f));
TreasureCell(holder, it, true);
var c = holder.GetChild(0) as RectTransform;
c.anchorMin = Vector2.zero;
c.anchorMax = Vector2.one;
c.offsetMin = c.offsetMax = Vector2.zero;
}
var fx = UiKit.Box("Effects", block.transform, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), Vector2.zero, new Vector2(1032f, 250f), UiKit.Paper);
UiKit.Border(fx, UiKit.Line, 2f);
var best = TreasureRules.SetTier(db, d, setId);
var sb = new System.Text.StringBuilder("<b>기본 효과</b> <size=26>(가장 높은 달성 단계 1개)</size>");
string okHex = "#" + ColorUtility.ToHtmlStringRGB(UiKit.Ok), offHex = "#" + ColorUtility.ToHtmlStringRGB(UiKit.InkSoft);
foreach (var t in tiers)
{
bool ok = TreasureRules.TierReached(d, t);
string label = t.stars == 0 ? "[세트]" : "[" + t.stars + "성]";
string stat = TreasureRules.StatKo(t.effectKey.Replace("_pct", ""));
sb.Append("\n").Append(ok ? (t == best ? "<color=" + okHex + "><b>" : "<color=" + okHex + ">") : "<color=" + offHex + ">").Append(label).Append(" ").Append(stat).Append(" +").Append(t.pct.ToString("0.#")).Append("%")
.Append(ok ? (t == best ? " ✔ 적용</b>" : " 달성") : " 미달성").Append("</color>");
}
UiKit.FillLabel(fx.transform, sb.ToString(), 30, UiKit.Ink, TextAnchor.MiddleLeft, false, 24f);
SetCount++;
}
}
// ---------- ③ 상자 ----------
void BuildChests(RectTransform p)
{
var db = S.Db;
var d = S.Save;
var e = db.Economy;
var goalBox = UiKit.TopBand("Points", p, 6f, 150f, 24f, 24f);
var gimg = UiKit.Paint(goalBox, UiKit.Paper);
UiKit.Border(gimg, UiKit.Line, 3f);
if (UiKit.Icon(goalBox, "chest_point", new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(16f, 0f), 100f) == null)
UiKit.FillLabel(UiKit.Dot("P", goalBox, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(20f, 0f), 90f, UiKit.Coin).transform, "P", 44, UiKit.Ink);
var reward = db.Chests.Get(e.ChestPointReward);
UiKit.Label(goalBox, (e.ChestPointGoal - d.inv.chestPoints) + "포인트 더 모으면 " + (reward != null ? reward.nameKo : ""), 34, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(130f, -12f), new Vector2(800f, 54f));
var fill = UiKit.Bar("Bar", goalBox, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(130f, 52f), new Vector2(700f, 30f), UiKit.Slot, UiKit.Coin);
UiKit.SetBar(fill, d.inv.chestPoints / (float)Mathf.Max(1, e.ChestPointGoal));
UiKit.Label(goalBox, "상자 포인트 " + d.inv.chestPoints + " / " + e.ChestPointGoal, 26, UiKit.InkSoft, TextAnchor.MiddleLeft, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(130f, 8f), new Vector2(700f, 40f), false);
var c = db.Chests.Get(SelectedChest);
if (c == null) return;
int count = TreasureRules.ChestCount(d, c.id);
var banner = UiKit.Box("Banner", p, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -180f), new Vector2(560f, 470f), ChestColor(c.id));
UiKit.Border(banner, new Color(0.25f, 0.2f, 0.18f), 4f);
UiKit.Title(UiKit.Label(banner.transform, c.nameKo, 60, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -40f), new Vector2(520f, 90f)), true);
UiKit.Icon(banner.transform, "chest_" + c.id, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(10f, -10f), 150f);
int line = 0;
for (int i = 0; i < ChestDb.ProbGrades.Length; i++)
{
if (c.p[i] <= 0f) continue;
var chip = UiKit.Box("P" + i, banner.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -150f - line * 62f), new Vector2(360f, 54f), GradeColor(ChestDb.ProbGrades[i]));
UiKit.FillLabel(chip.transform, TreasureRules.GradeKo(ChestDb.ProbGrades[i]) + " " + (c.p[i] * 100f).ToString("0.#") + " %", 30, UiKit.White);
line++;
}
var extra = new List<string>();
if (c.goldMax > 0) extra.Add("골드 " + c.goldMin + "~" + c.goldMax);
if (c.gemMax > 0) extra.Add("보옥 " + c.gemMin + "~" + c.gemMax);
if (c.ironMax > 0) extra.Add("운철 " + c.ironMin + "~" + c.ironMax);
extra.Add("포인트 +" + c.points);
UiKit.Label(banner.transform, string.Join(" · ", extra.ToArray()), 26, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 18f), new Vector2(540f, 50f), false);
UiKit.Btn("Prev", p, "◁", 70, new Color(0f, 0f, 0f, 0f), UiKit.Ink, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(40f, -360f), new Vector2(120f, 140f), () => Step(-1));
UiKit.Btn("Next", p, "▷", 70, new Color(0f, 0f, 0f, 0f), UiKit.Ink, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-40f, -360f), new Vector2(120f, 140f), () => Step(1));
var open = UiKit.Btn("Open", p, count > 0 ? "열기\n<size=28>보유 " + count + "</size>" : "열기\n<size=28>상자 없음</size>", 46, count > 0 ? UiKit.Red : UiKit.Disabled, UiKit.White,
new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -680f), new Vector2(480f, 140f), () => DoOpen(SelectedChest));
open.interactable = count > 0;
UiKit.Label(p, "상자는 이 탭에서만 엽니다 · 중복 = 조각 · 천기 = 적장 토벌 전용", 26, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -836f), new Vector2(1000f, 44f), false);
int n = db.Chests.chests.Count;
float w = (1032f - (n - 1) * 12f) / n;
for (int i = 0; i < n; i++)
{
var ch = db.Chests.chests[i];
int k = i;
var b = UiKit.Btn("Chest_" + ch.id, p, "", 28, i == chestIdx ? UiKit.Red : UiKit.Paper, UiKit.Ink, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(24f + i * (w + 12f), 24f), new Vector2(w, 220f), () =>
{
chestIdx = k;
chestPicked = true;
Refresh();
});
UiKit.Border(b.GetComponent<Image>(), i == chestIdx ? UiKit.Coin : UiKit.Line, i == chestIdx ? 5f : 2f);
var box = UiKit.Box("Icon", b.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -20f), new Vector2(w * 0.7f, 90f), ChestColor(ch.id));
box.raycastTarget = false;
UiKit.Icon(box.transform, "chest_" + ch.id, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 4f), 96f);
UiKit.Label(b.transform, ch.nameKo + "\n<size=34>×" + TreasureRules.ChestCount(d, ch.id) + "</size>", 26, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 14f), new Vector2(w, 100f));
}
}
/// <summary>
/// 상자 탭(Fable 제안서 §4-5 · #1104) — 포인트 띠(BannerFrame05 · 가는 주황 바 · 「85 / 120」 · 오른쪽 보상 상자) →
/// 상자 카드(CardFrame03 상자 색 + 안쪽 + 광택 · 상자 아이콘 300 + 뒤 금빛 · 등급 칩 + % 한 줄 · 내용물 줄) · 좌우 화살표 →
/// 「열기」 Primary 노랑 + 보유 칩(없으면 회색) → 아래 상자 종류 행(ItemFrame 5층 상자 색 + 수량 칩 · 선택 = FocusLine 깜빡).
/// </summary>
void BuildChestsKit(RectTransform p)
{
var db = S.Db;
var d = S.Save;
var e = db.Economy;
var soft = UiKit.Col("text_soft", UiKit.InkSoft);
// 포인트 띠
var band = UiKit.Skin(UiKit.At(p, "Points", 24f, 6f, 1032f, 92f), "banner_point", UiKit.Col("banner_point", UiKit.White)).transform;
var pi = UiKit.Icon(band, "chest_point", new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(46f, 2f), 76f);
if (pi != null) UiKit.DropShadow(pi, 3f);
var reward = db.Chests.Get(e.ChestPointReward);
UiKit.Label(band, Mathf.Max(0, e.ChestPointGoal - d.inv.chestPoints) + "포인트 더 모으면 " + (reward != null ? reward.nameKo : ""), 28, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(112f, -6f), new Vector2(640f, 44f));
var bg = UiKit.Skin(UiKit.At(band, "Bar", 112f, 50f, 600f, 30f), "bar4_bg", UiKit.White);
var fill = UiKit.Skin(UiKit.At(bg.transform, "Fill", 4f, 4f, 592f, 20f), "bar4_fill", UiKit.White).rectTransform;
float v = Mathf.Clamp01(d.inv.chestPoints / (float)Mathf.Max(1, e.ChestPointGoal));
fill.sizeDelta = new Vector2(Mathf.Max(16f, 592f * v), 20f);
fill.gameObject.SetActive(v > 0.001f);
var pt = UiKit.Title(UiKit.Label(band, d.inv.chestPoints + " / " + e.ChestPointGoal, 30, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(730f, -38f), new Vector2(180f, 50f)), true);
pt.horizontalOverflow = HorizontalWrapMode.Overflow;
if (reward != null)
{
var ri = UiKit.Icon(band, "chest_" + reward.id, new Vector2(1f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(-62f, 4f), 80f);
if (ri != null) UiKit.DropShadow(ri, 3f);
}
if (!chestPicked && TreasureRules.ChestCount(d, SelectedChest) == 0)
for (int i = 0; i < db.Chests.chests.Count; i++)
if (TreasureRules.ChestCount(d, db.Chests.chests[i].id) > 0)
{
chestIdx = i;
break;
}
var c = db.Chests.Get(SelectedChest);
if (c == null) return;
int count = TreasureRules.ChestCount(d, c.id);
// 무대 = 카드 뒤 문양 흰 5 % + 바닥 그라데이션(검수2 결함 4)
if (UISkin.Has("bg_pattern"))
{
var stage = UiKit.At(p, "Stage", 0f, 108f, 1080f, 1000f);
var pat = UiKit.Skin(stage, "bg_pattern", UiKit.Col("stage_pattern", Color.clear));
pat.type = Image.Type.Tiled;
if (UISkin.Has("scroll_fade"))
{
var g = UiKit.Skin(UiKit.At(stage, "Ground", 0f, 1000f - 160f, 1080f, 160f), "scroll_fade", UiKit.Col("panel_ground", Color.clear));
g.rectTransform.localScale = new Vector3(1f, -1f, 1f);
g.rectTransform.pivot = new Vector2(0f, 0f);
}
}
// 상자 카드
const float Cw = 680f, Ch = 760f, Cy = 120f;
var card = UiKit.At(p, "Card", (1080f - Cw) * 0.5f, Cy, Cw, Ch);
UiKit.Skin(card, "card3_bg", ChestColor(c.id));
UiKit.Layer(card, "Inner", "card3_inner", UiKit.Col("card3_inner", Color.clear), 18f, 104f, 18f, 18f);
UiKit.Layer(card, "Light", "card3_light", UiKit.Col("card3_light", Color.clear), 6f, 6f, 6f, 10f);
var name = UiKit.Title(UiKit.Label(card, c.nameKo, 56, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -16f), new Vector2(Cw - 40f, 80f)), true);
name.horizontalOverflow = HorizontalWrapMode.Overflow;
if (UISkin.Has("glow_circle"))
{
var glow = UiKit.Skin(UiKit.Rect("Glow", card, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -320f), new Vector2(600f, 600f)), "glow_circle", UiKit.Col("chest_glow", Color.clear));
var pulse = glow.gameObject.AddComponent<UiPulse>();
pulse.min = 0.7f;
pulse.period = 1.6f;
}
var ci = UiKit.Icon(card, "chest_" + c.id, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -320f), 320f);
if (ci != null) UiKit.DropShadow(ci, 8f);
var probs = new List<int>();
for (int i = 0; i < ChestDb.ProbGrades.Length; i++)
if (c.p[i] > 0f) probs.Add(i);
const float Pw = 178f, Pg = 14f;
float px = (Cw - probs.Count * Pw - (probs.Count - 1) * Pg) * 0.5f;
for (int k = 0; k < probs.Count; k++)
{
int i = probs[k];
string g = ChestDb.ProbGrades[i];
var chip = UiKit.Skin(UiKit.At(card, "P" + i, px + k * (Pw + Pg), 528f, Pw, 60f), "para_orange", GradeColor(g));
var ct = UiKit.Title(UiKit.FillLabel(chip.transform, TreasureRules.GradeKo(g) + " " + (c.p[i] * 100f).ToString("0.#") + "%", 30, UiKit.White), true);
ct.horizontalOverflow = HorizontalWrapMode.Overflow;
}
var extra = new List<string>();
if (c.goldMax > 0) extra.Add("골드 " + c.goldMin + "~" + c.goldMax);
if (c.gemMax > 0) extra.Add("보옥 " + c.gemMin + "~" + c.gemMax);
if (c.ironMax > 0) extra.Add("운철 " + c.ironMin + "~" + c.ironMax);
extra.Add("포인트 +" + c.points);
// 내용물 줄 = 카드 아래 어두운 띠 위 흰 글자(검수2 결함 10)
var eb = UiKit.Layer(card, "ExtraBand", "card_bottom", UiKit.Col("card_bottom", UiKit.Dark), 6f, 0f, 6f, 6f);
eb.rectTransform.anchorMax = new Vector2(1f, 0f);
eb.rectTransform.offsetMax = new Vector2(-6f, 76f);
var et = UiKit.Title(UiKit.FillLabel(eb.transform, string.Join(" · ", extra.ToArray()), 26, UiKit.White, TextAnchor.MiddleCenter, true, 16f), true);
et.horizontalOverflow = HorizontalWrapMode.Overflow;
// 좌우 화살표(카드 가운데 높이)
Arrow(p, "Prev", "arrow_prev", 30f, Cy + Ch * 0.5f - 70f, () => Step(-1));
Arrow(p, "Next", "arrow_next", 1080f - 30f - 100f, Cy + Ch * 0.5f - 70f, () => Step(1));
// 열기
var open = UiKit.SkinBtn(p, "Open", count > 0 ? "btn_l_yellow" : "btn_gray", UiKit.White, count > 0 ? "열기" : "상자 없음", 46,
count > 0 ? UiKit.Col("text_on_yellow", UiKit.Ink) : UiKit.White, (1080f - 460f) * 0.5f, Cy + Ch + 26f, 460f, 116f, () => DoOpen(SelectedChest));
open.interactable = count > 0;
if (count > 0) UiKit.BtnCost(open, "chest_" + c.id, "x" + count, 56f, 34);
UiKit.Label(p, "상자는 이 탭에서만 엽니다 · 중복 = 조각 · 천기 = 적장 토벌 전용", 26, soft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -(Cy + Ch + 156f)), new Vector2(1000f, 44f), false);
// 상자 종류 행(아래)
int n = db.Chests.chests.Count;
const float Tw = 150f;
float gap = n > 1 ? Mathf.Min(64f, (1032f - n * Tw) / (n - 1)) : 0f;
float tx0 = (1080f - n * Tw - (n - 1) * gap) * 0.5f;
float ty = OutGameRoot.BodyH - 118f - 24f - 40f - Tw;
for (int i = 0; i < n; i++)
{
var ch = db.Chests.chests[i];
int k = i;
bool sel = i == chestIdx;
var cell = UiKit.At(p, "Chest_" + ch.id, tx0 + i * (Tw + gap), ty, Tw, Tw);
UiKit.ItemSlot(cell, "chest_" + ch.id + "_", sel);
var hit = cell.gameObject.AddComponent<Image>();
hit.color = Color.clear;
cell.gameObject.AddComponent<Button>().onClick.AddListener(() =>
{
chestIdx = k;
chestPicked = true;
Refresh();
});
var ic = UiKit.Icon(cell, "chest_" + ch.id, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 6f), 104f);
if (ic != null) UiKit.DropShadow(ic, 4f);
UiKit.QtyChip(cell, "x" + TreasureRules.ChestCount(d, ch.id), 28);
if (sel && UISkin.Has("focus_line"))
{
var f = UiKit.Layer(cell, "Focus", "focus_line", UiKit.Col("chest_select", UiKit.White), -8f, -8f, -8f, -8f);
f.gameObject.AddComponent<UiPulse>();
cell.localScale = Vector3.one * 1.08f;
}
else UiKit.Layer(cell, "Dim", "item_bg", UiKit.Col("chest_dim", new Color(0f, 0f, 0f, 0.25f)));
var nl = UiKit.Label(p, ch.nameKo, 26, sel ? UiKit.White : soft, TextAnchor.UpperCenter, new Vector2(0f, 1f), new Vector2(0.5f, 1f), new Vector2(tx0 + i * (Tw + gap) + Tw * 0.5f, -(ty + Tw + 10f)), new Vector2(Tw + gap, 36f), sel);
nl.horizontalOverflow = HorizontalWrapMode.Overflow;
}
}
/// <summary>화살표 버튼(투명 판 + 팩 화살표 아이콘 · 없으면 글자).</summary>
static void Arrow(Transform p, string name, string icon, float x, float y, System.Action onClick)
{
var rt = UiKit.At(p, name, x, y, 100f, 140f);
var img = rt.gameObject.AddComponent<Image>();
img.color = Color.clear;
rt.gameObject.AddComponent<Button>().onClick.AddListener(() => onClick());
var ic = UiKit.Icon(rt, icon, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 96f);
if (ic != null) UiKit.DropShadow(ic, 4f);
else UiKit.Title(UiKit.FillLabel(rt, name == "Prev" ? "◁" : "▷", 70, UiKit.White), true);
}
void Step(int dir)
{
int n = Mathf.Max(1, S.Db.Chests.chests.Count);
chestIdx = ((chestIdx + dir) % n + n) % n;
chestPicked = true;
Refresh();
}
public void DoOpen(string id)
{
var r = S.OpenChest(id);
LastOpen = r;
if (r == null)
{
Root.Toast("상자 없음");
return;
}
var it = r.treasureId != null ? S.Db.Treasures.Get(r.treasureId) : null;
var sb = new System.Text.StringBuilder();
if (it != null)
sb.Append("<size=48><b>").Append(it.nameKo).Append("</b></size>\n").Append(TreasureRules.GradeKo(it.grade)).Append(r.gradeUp ? " (상위 등급!)" : "")
.Append(r.isNew ? " <color=#b0301e>NEW</color>" : " 조각 +" + r.shards).Append("\n\n");
var parts = new List<string>();
if (r.gold > 0) parts.Add("골드 +" + r.gold.ToString("N0"));
if (r.gem > 0) parts.Add("보옥 +" + r.gem.ToString("N0"));
if (r.iron > 0) parts.Add("운철 +" + r.iron);
parts.Add("상자 포인트 +" + r.points);
sb.Append(string.Join(" · ", parts.ToArray()));
if (r.bonusChests > 0) sb.Append("\n<color=#b0301e>포인트 달성 → ").Append(S.Db.Chests.Get(S.Db.Economy.ChestPointReward).nameKo).Append(" +").Append(r.bonusChests).Append("</color>");
Root.Modal(S.Db.Chests.Get(id).nameKo + " 열기", sb.ToString(),
new OutGameRoot.ModalButton("한 번 더", UiKit.Red, () => DoOpen(id)),
new OutGameRoot.ModalButton("확인", UiKit.Dark, null));
}
/// <summary>상자 대표 색 = 테마 chest_&lt;id&gt;(구 코드 상수 → ui_theme.csv · C45 · #1104).</summary>
public static Color ChestColor(string id)
{
return UiKit.Col("chest_" + id, UiKit.Slot);
}
}
}