using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace TKS.OutGame
{
///
/// 보물(하단 탭 · 기획 S8 · 목업 img07·16·17 · 카피바라 수집품 구조 · PD #1093 C) — 상단 탭 3:
/// ① 보유 중 = 그리드 4열 · 등급 색 · ★ · 미보유 회색 · 탭 = 상세(기본 효과 · 조건 효과 n/max · 강화 ★)
/// ② 세트 = 세트마다 구성 · 조각 게이지 · [세트]/[3성]/[6성]/[10성] 효과 ③ 상자 = 상자 포인트 게이지 → 명장 상자 · 선택 상자 + 확률 · 보유 수 · 열기(상자는 이 탭에서만 연다).
///
public class TreasureScreen : OutGameScreen
{
public static readonly string[] PageNames = { "보유 중", "세트", "상자" };
int page;
int chestIdx;
bool chestPicked;
string gradeFilter = ""; // 보유 중 등급 거르기("" = 전체 · PD 2026-11-13 「등급 칩 줄 → 드롭다운 · 전체 ~ 등급별」)
RectTransform filterList;
readonly List tabs = new List();
readonly List pages = new List();
/// 배경 장면(PD 2026-11-13 · vault = 보물고 · chest = 금고) — 처음 볼 때 만들고 숨김/보임만.
class Scene
{
public List rows;
public LobbyAmbient amb;
public RectTransform art;
public float artW;
public float artH;
}
RectTransform backdrop;
readonly Dictionary scenes = new Dictionary();
Image openChestIcon;
/// 지금 보이는 배경 장면 이름(그림이 없으면 null · 테스트).
public string ShownScene { get; private set; }
/// 보유 중·세트 칸을 진열대(칸 틀 + 선반)로 그렸는가(테스트).
public bool ShelfCells { get; private set; }
/// 이 페이지에 그린 목패 이름표 수(테스트 · 리본 폐기 3차).
public int NameTags { get; private set; }
/// 세트 제목을 명패로 그린 수(테스트 · 3차).
public int Plaques { get; private set; }
/// 상자 탭을 금고 받침대 배치로 그렸는가(테스트).
public bool ChestOnPedestal { get; private set; }
/// 받침대 위 상자 아이콘(테스트).
public Image PedestalChest
{
get { return openChestIcon; }
}
public int Page
{
get { return page; }
}
public int GridCount { get; private set; }
/// 보유 중 등급 거르기(""= 전체 · 테스트).
public string GradeFilter
{
get { return gradeFilter; }
}
/// 등급 드롭다운 목록이 펼쳐져 있는가(테스트).
public bool FilterOpen
{
get { return filterList != null; }
}
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();
backdrop = UiKit.Fill("Backdrop", body); // 배경 장면(보물고 · 금고 · 탭·페이지 뒤 · PD 2026-11-13)
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));
}
}
/// 새 상자 화면 부품(Fable §4-5)이 테마에 있는가.
static bool Kit
{
get { return UISkin.Has("card3_bg") && UISkin.Has("banner_point") && UISkin.Has("btn_l_yellow"); }
}
/// 보물 아이콘 id — 납품 그림(tre_<id>)이 있으면 그것 · 없으면 기본 스탯별 팩 임시 아이콘(tre_stat_* · Fable §6 N3 전까지).
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().color = on ? UiKit.Red : UiKit.Paper; // 선택 = 붉은 옻칠(다른 화면 탭과 같음)
UiKit.LabelOf(tabs[i]).color = on ? UiKit.White : UiKit.Ink;
}
pages[i].gameObject.SetActive(on);
}
var scene = ShowScene(TreasureScene.PageScene[Mathf.Clamp(page, 0, TreasureScene.PageScene.Length - 1)]);
UiKit.Clear(pages[page]);
filterList = null; // 펼친 목록은 페이지와 함께 지워짐
openChestIcon = null;
ShelfCells = false;
NameTags = 0;
Plaques = 0;
ChestOnPedestal = false;
if (page == 0) BuildOwned(pages[0]);
else if (page == 1) BuildSets(pages[1]);
else if (scene != null && Kit) BuildChestsVault(pages[2], scene);
else if (Kit) BuildChestsKit(pages[2]);
else BuildChests(pages[2]);
}
/// 그 장면을 (처음이면 만들고) 보이고 나머지는 숨김 · 그림이 없으면 null(예전 바탕).
Scene ShowScene(string name)
{
Scene s;
if (!scenes.TryGetValue(name, out s))
{
s = new Scene { rows = TreasureScene.Scene(name) };
s.amb = DomesticScene.Backdrop(backdrop, "Scene_" + name, s.rows, out s.art, out s.artW, out s.artH);
scenes[name] = s;
}
foreach (var kv in scenes)
if (kv.Value.art != null)
kv.Value.art.parent.gameObject.SetActive(kv.Key == name);
ShownScene = s.art != null ? name : null;
return s.art != null ? s : null;
}
/// 보물고 진열장 배경이 보일 때 그 장면 표 줄(아니면 null = 예전 배치).
LobbyScene.Row VaultRow(string kind)
{
Scene s;
return ShownScene == "vault" && scenes.TryGetValue("vault", out s) ? DomesticScene.First(s.rows, kind) : null;
}
/// 장면 그림 px → 페이지 좌표(본문 = 1080 폭 설계 단위 · 페이지 = 본문 위 118 아래 · view f_y 줄이 본문 맨 위 · f_scale 배).
static Vector2 ArtToPage(Scene s, float x, float y)
{
var view = DomesticScene.First(s.rows, "view");
float vy = view != null ? view.y : 0f;
float z = view != null && view.scale > 0.01f ? view.scale : 1f;
float k = OutGameRoot.Reference.x * z / Mathf.Max(1f, s.artW); // 그림 px → 본문 px
return new Vector2(OutGameRoot.Reference.x * 0.5f + (x - s.artW * 0.5f) * k, (y - vy) * k - 118f);
}
/// 보물 등급 대표 색(테마 tre_<등급> · 희귀 청 · 영웅 자 · 전설 주 · 신화 적 · 명장 금).
public static Color GradeColor(string g)
{
return UiKit.Col("tre_" + g, UiKit.Slot);
}
///
/// 보물 칸(UI 상업용 재설계 ⑥ · 설계서 §3⑥) = 팩 ItemFrame 5층 등급색 + 아이콘(납품 tre_* · 없으면 이름 첫 글자) + ★ 또는 조각 바 + 아래 이름 판 ·
/// 미보유 = 진청 빈 칸 + 흰 25 % 실루엣.
///
/// 보물 칸 아래 이름 띠 높이(칸 아래 끝에서 · 아이콘 자리는 이 위 색 칸).
const float NameBandH = 62f;
/// 조각 막대·별 줄 윗변(칸 아래 끝에서 · 막대 y 76 + 높이 26 = 별 줄 y 72 + 30).
const float ShardsTop = 102f;
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;
// 보물고 진열장 배경일 때 = 선반 진열(PD 2026-11-13 2차 「칸 틀이 튐 → 레퍼런스 게임 유물 목록처럼 가독성 · 내정처럼 공간 인지」) —
// 틀 없는 큰 아이콘이 이어진 선반 판 위에 서고, 선반 앞에 등급 색 목패 이름표(PD 2026-11-13 3차 · 리본 폐기)
Scene vs;
var shelfRow = ShownScene == "vault" && scenes.TryGetValue("vault", out vs) ? DomesticScene.First(vs.rows, "shelf") : null;
var tagRow = shelfRow != null ? DomesticScene.First(scenes["vault"].rows, "tag") : null;
var shelfSp = shelfRow != null ? UISkin.Icon(shelfRow.sprite) : null;
var tagSp = tagRow != null ? UISkin.Icon(tagRow.sprite) : null;
if (shelfSp != null && tagSp != null)
{
ShelfCells = true;
ShelfCell(t, it, owned, showShards, shelfRow, tagRow, shelfSp, tagSp);
return;
}
UiKit.ItemSlot(t, owned ? "tre_" + it.grade + "_" : "tre_none_", owned);
// 아이콘 자리 = 이름 띠 위 색 칸 전체 → 아이콘은 그 정가운데(PD 2026-11-11 「정중앙 · 20 % 키움」 · 전에는 위에서 100 px 고정 = 14 px 위로 뜸)
// 세트 화면(조각 막대 표시)은 막대 위부터 — 커진 아이콘 아래를 막대가 가리지 않게
var art = UiKit.Rect("Art", t, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero);
art.anchorMin = Vector2.zero;
art.anchorMax = Vector2.one;
art.offsetMin = new Vector2(0f, showShards ? ShardsTop : NameBandH);
art.offsetMax = Vector2.zero;
string icon = IconId(it);
if (icon != null)
{
// 아이콘 = 납품 그림 또는 기본 스탯별 임시(Fable §4-5 「글자 → 보물 아이콘」) · 미보유 = 흰 25 % 실루엣 · 크기 = 예전 140/120 × 1.2
var ic = UiKit.Icon(art, icon, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, icon.StartsWith("tre_stat_") ? 144f : 168f);
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);
CellFooter(t, it, st, owned, showShards);
}
///
/// 선반 진열 칸(PD 2026-11-13 2차 · 레퍼런스 게임 유물 목록 + 보물고 진열장) — 표 vault_shelf · vault_tag · vault_icon · vault_grid/setgrid:
/// 선반 판(칸 간격 절반씩 옆으로 = 한 줄이 이어진 선반) → 틀 없는 큰 아이콘(판 윗면에 섬 · 미보유 = 어둡게) → 선반 앞 등급 색 목패 이름표(미보유 = 어두운 목패 · 3차) → 별·조각 막대(판 앞면).
///
void ShelfCell(Transform t, TreasureDb.Item it, bool owned, bool showShards, LobbyScene.Row shelfRow, LobbyScene.Row tagRow, Sprite shelfSp, Sprite tagSp)
{
var st = S.Save.inv.Treasure(it.id);
var rows = scenes["vault"].rows;
var grid = DomesticScene.First(rows, showShards ? "setgrid" : "grid");
var iconRow = DomesticScene.First(rows, "icon");
float gapX = grid != null && grid.gapMin > 0f ? grid.gapMin : 16f;
float cellW = grid != null && grid.w > 1f ? grid.w : 200f;
float shelfY = shelfRow.y > 0f ? shelfRow.y : 58f;
float shelfH = shelfRow.h > 0f ? shelfRow.h : 30f;
ShelfBoard(t, shelfRow, shelfSp, gapX);
string icon = IconId(it);
float size = iconRow != null ? (showShards ? iconRow.h : iconRow.w) : 160f;
if (size < 1f) size = 160f;
float stand = shelfY + shelfH * 0.6f; // 아이콘 아래끝 = 판 윗면
if (icon != null)
{
var ic = UiKit.Icon(t, icon, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, stand), icon.StartsWith("tre_stat_") ? size * 0.86f : size);
if (owned) UiKit.DropShadow(ic, 5f);
else
{
// 미보유 = 밝은 회색 틴트(표 vault_none · 어두운 판에 묻히지 않게 · QA r3 M1)
var none = DomesticScene.First(rows, "none");
ic.color = none != null ? none.color : UiKit.Col("tre_none_icon", UiKit.InkSoft);
}
}
else if (owned)
{
var art = UiKit.Rect("Art", t, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, stand), new Vector2(size, size));
UiKit.Title(UiKit.FillLabel(art, it.nameKo.Substring(0, 1), 84, UiKit.White), true);
}
// 미보유도 유물 이름(흐린 글자 · 레퍼런스 목록처럼 무엇인지 보임 · QA r3 M7) — 미보유 = 어두운 목패 + 흐린 아이콘으로 구분 · 폭 = 글자에 맞춤(칸 폭 안)
NameTag(t, it.nameKo, owned ? GradeColor(it.grade) : UiKit.Col("tre_none_tag", new Color(0.36f, 0.33f, 0.36f, 1f)),
owned ? UiKit.White : UiKit.Col("text_soft", UiKit.InkSoft), tagRow, tagSp, cellW, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, tagRow.y));
NameTags++;
if (owned)
{
// 진행 수치 = 최고 ★ 전이면 늘 조각 막대(레퍼런스 목록 「5/40」 · QA r3 U1) · 최고 ★ = 별 줄
if (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, shelfY + 2f), new Vector2(Mathf.Min(150f, size), 24f), UiKit.Slot, GradeColor(it.grade));
UiKit.SetBar(fill, st.shards / (float)Mathf.Max(1, need));
UiKit.Title(UiKit.FillLabel(fill.parent, st.shards + "/" + need, 18, UiKit.White), true);
}
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, shelfY + 2f), 26f);
}
}
/// 칸 아래 선반 판 한 토막(표 vault_shelf · 칸 간격 절반 + 2 px 씩 옆으로 = 옆 칸 판과 겹쳐 한 줄로 이어짐 · QA r3 M3).
static void ShelfBoard(Transform t, LobbyScene.Row shelfRow, Sprite shelfSp, float gapX)
{
float shelfY = shelfRow.y > 0f ? shelfRow.y : 58f;
float shelfH = shelfRow.h > 0f ? shelfRow.h : 30f;
var shelf = UiKit.Rect("Shelf", t, Vector2.zero, Vector2.zero, Vector2.zero, Vector2.zero);
shelf.anchorMin = Vector2.zero;
shelf.anchorMax = new Vector2(1f, 0f);
shelf.offsetMin = new Vector2(-gapX * 0.5f - 2f, shelfY);
shelf.offsetMax = new Vector2(gapX * 0.5f + 2f, shelfY + shelfH);
var si = shelf.gameObject.AddComponent();
si.sprite = shelfSp;
si.raycastTarget = false;
if (shelfSp.border.sqrMagnitude > 0f) si.type = Image.Type.Sliced;
}
///
/// 마지막 줄 빈 칸 = 선반 판만(QA r4 M3 「거른 목록 마지막 줄 판이 한 칸만 깔리고 끝이 잘려 떠 보임」) — 줄 끝까지 판이 이어지게. 반환 = 채운 칸 수.
///
int FillShelfRow(Transform grid, int count, int cols)
{
Scene vs;
if (!ShelfCells || cols < 2 || count % cols == 0 || !scenes.TryGetValue("vault", out vs)) return 0;
var shelfRow = DomesticScene.First(vs.rows, "shelf");
var g = DomesticScene.First(vs.rows, "grid");
var shelfSp = shelfRow != null ? UISkin.Icon(shelfRow.sprite) : null;
if (shelfSp == null) return 0;
int n = cols - count % cols;
for (int i = 0; i < n; i++)
{
var cell = UiKit.Cell(grid, "ShelfOnly" + i, Color.clear, null);
cell.sprite = null;
cell.raycastTarget = false;
ShelfBoard(cell.transform, shelfRow, shelfSp, g != null && g.gapMin > 0f ? g.gapMin : 16f);
}
return n;
}
///
/// 등급 색 목패 이름표(PD 2026-11-13 3차 「리본 폐기 — 불필요한 면적이 많고 글자가 길면 UI 침범 · 우리 게임 컨셉·화풍으로」) — 표 <장면>_tag:
/// s_sprite = 목패(밝은 회색 면 = 등급 색으로 물들임 · 좌우 끝만 고정 9 분할) · f_h = 높이 · f_scale = 글자 · f_min = 가장 작은 글자 · f_x = 글자 좌우 여백 · f_w = 가장 좁은 폭.
/// 폭 = 글자 폭 + 여백(maxW 까지) · maxW 에서도 넘치면 글자를 줄여 목패 안에 맞춤(한 줄 · 밖으로 나가지 않음).
///
public static Text NameTag(Transform parent, string label, Color tint, Color ink, LobbyScene.Row row, Sprite sp, float maxW, Vector2 anchor, Vector2 pivot, Vector2 pos)
{
float h = row.h > 1f ? row.h : 52f;
int fs = row.scale > 1f ? Mathf.RoundToInt(row.scale) : 24;
int fsMin = row.min > 1f ? Mathf.RoundToInt(row.min) : 16;
float pad = row.x > 0f ? row.x : 14f;
var tag = UiKit.Rect("Tag", parent, anchor, pivot, pos, new Vector2(maxW, h));
var img = tag.gameObject.AddComponent();
img.sprite = sp;
img.color = tint;
img.raycastTarget = false;
if (sp.border.sqrMagnitude > 0f) img.type = Image.Type.Sliced;
var t = UiKit.Title(UiKit.FillLabel(tag, label, fs, ink, TextAnchor.MiddleCenter, true, pad), true);
t.horizontalOverflow = HorizontalWrapMode.Overflow;
t.rectTransform.offsetMin = new Vector2(pad, h * 0.06f); // 목패 아래 테가 두꺼워 글자를 조금 올림
float need = t.preferredWidth;
if (need + pad * 2f > maxW && need > 1f)
t.fontSize = Mathf.Max(fsMin, Mathf.FloorToInt(fs * (maxW - pad * 2f) / need));
float minW = Mathf.Min(row.w > 1f ? row.w : 0f, maxW);
tag.sizeDelta = new Vector2(Mathf.Clamp(t.preferredWidth + pad * 2f, minW, maxW), h);
return t;
}
/// 칸 아래 = 별 줄 또는 조각 막대 + 이름 띠(예전 칸 · 진열대 칸 공통).
void CellFooter(Transform t, TreasureDb.Item it, TreasureState st, bool owned, bool showShards)
{
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, NameBandH);
}
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;
var grades = db.Economy.TreasureGrades;
if (gradeFilter.Length > 0 && db.Economy.TreasureGradeOrder(gradeFilter) == 0) gradeFilter = ""; // 표에서 사라진 등급 = 전체
// 보유 수 = 고른 등급 안에서(전체면 전부)
int owned = 0, total = 0;
foreach (var it in db.Treasures.items)
{
if (gradeFilter.Length > 0 && it.grade != gradeFilter) continue;
total++;
if (S.Save.inv.Treasure(it.id) != null) owned++;
}
var cnt = UiKit.Title(UiKit.Label(p, "보유 " + owned + " / " + total, 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;
// 등급 = 오른쪽 위 드롭다운 하나(PD 2026-11-13 「등급 칩 줄이 진열장 왕관 가운데를 가림 → 드롭다운 · 전체 ~ 등급별」 · 펼친 목록 = 등급 색 칩 = 범례)
GradeFilterButton(p);
ScrollRect sr;
// 보물고 진열장 배경 = 진열장 안쪽 판에 맞춘 그리드(표 vault_grid · 위 = 진열장 왕관이 보이게) · 아니면 예전 그리드
var g = VaultRow("grid");
var grid = g != null
? UiKit.ScrollGrid("Grid", p, g.x, g.x, g.y, 64f, new Vector2(g.w, g.h), new Vector2(g.gapMin, g.gapMax), Mathf.Max(1, g.count), out sr)
: UiKit.ScrollGrid("Grid", p, 24f, 24f, 76f, 64f, new Vector2(240f, 290f), new Vector2(16f, 16f), 4, out sr);
// 정렬 = 보유 먼저 · 등급 높은 순
var list = new List(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)
{
if (gradeFilter.Length > 0 && it.grade != gradeFilter) continue;
TreasureCell(grid, it, false);
GridCount++;
}
if (g != null) FillShelfRow(grid, GridCount, g.count); // 마지막 줄 빈 칸에도 선반 판(QA r4 M3)
var bonus = new List();
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);
}
// ---------- 보유 중 등급 드롭다운(PD 2026-11-13) ----------
/// 표 vault_filter — 오른쪽 여백 · 위 · 버튼 폭·높이 · 글자 · 목록 한 줄 높이 · 줄 간격 · 화살표 아이콘(진열장 배경이 아니면 기본값).
void FilterMetrics(out Vector2 at, out Vector2 size, out float rowH, out float gap, out int fs, out string arrow)
{
var r = VaultRow("filter");
at = new Vector2(r != null && r.x > 0f ? r.x : 24f, r != null ? r.y : 8f);
size = new Vector2(r != null && r.w > 1f ? r.w : 250f, r != null && r.h > 1f ? r.h : 56f);
rowH = r != null && r.min > 1f ? r.min : 60f;
gap = r != null && r.gapMin > 0f ? r.gapMin : 6f;
fs = r != null && r.scale > 1f ? Mathf.RoundToInt(r.scale) : 26;
arrow = r != null && r.sprite.Length > 0 ? r.sprite : "arrow_down";
}
/// 오른쪽 위 등급 버튼(지금 고른 등급 · 전체 = 어두운 판 · 등급 = 그 등급 색) + ▼.
void GradeFilterButton(RectTransform p)
{
Vector2 at, size;
float rowH, gap;
int fs;
string arrow;
FilterMetrics(out at, out size, out rowH, out gap, out fs, out arrow);
bool all = gradeFilter.Length == 0;
var img = UiKit.Skin(UiKit.Rect("Filter", p, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-at.x, -at.y), size), "subtab", all ? UiKit.Col("subtab_off", UiKit.Dark) : GradeColor(gradeFilter), true);
img.gameObject.AddComponent().onClick.AddListener(ToggleGradeFilter);
float aw = size.y * 0.6f;
var t = UiKit.Title(UiKit.Label(img.transform, all ? "전체" : TreasureRules.GradeKo(gradeFilter), fs, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(12f, 2f), new Vector2(size.x - aw - 40f, size.y)), true);
t.horizontalOverflow = HorizontalWrapMode.Overflow;
var ic = UiKit.Icon(img.transform, arrow, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-16f, 2f), aw);
if (ic != null) ic.raycastTarget = false;
filterArrow = ic != null ? ic.rectTransform
: UiKit.Label(img.transform, "▼", fs, UiKit.White, TextAnchor.MiddleCenter, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-16f, 2f), new Vector2(aw, size.y)).rectTransform;
filterArrow.pivot = new Vector2(0.5f, 0.5f); // 펼치면 ▲(180° · QA r4 M4) — 가운데 기준으로 돌림
filterArrow.anchoredPosition = new Vector2(-16f - aw * 0.5f, 2f);
}
RectTransform filterArrow;
/// 등급 드롭다운 펼침/접음(버튼 · 테스트).
public void ToggleGradeFilter()
{
if (filterList != null)
{
CloseGradeFilter();
return;
}
if (page != 0) return;
Vector2 at, size;
float rowH, gap;
int fs;
string arrow;
FilterMetrics(out at, out size, out rowH, out gap, out fs, out arrow);
// 펼친 목록 = 누를 때만 생기는 Event 층(P41) · 페이지 전체 투명 막 = 바깥 누르면 접힘
filterList = UiKit.Fill("FilterList", pages[0]);
UiLayer.Nest(filterList.gameObject, UiRate.Event, true);
UiKit.Paint(filterList, Color.clear, false, true);
filterList.gameObject.AddComponent().onClick.AddListener(CloseGradeFilter);
var grades = S.Db.Economy.TreasureGrades;
int n = grades.Count + 1;
float pad = gap + 4f;
var panel = UiKit.Skin(UiKit.Rect("Panel", filterList, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-at.x, -(at.y + size.y + gap)), new Vector2(size.x, pad * 2f + n * rowH + (n - 1) * gap)), "panel_field", UiKit.Col("panel_field", UiKit.Dark), true);
UiKit.DropShadow(panel, 6f); // 아래 목록과 떨어져 보이게(QA r4 M4)
if (filterArrow != null) filterArrow.localEulerAngles = new Vector3(0f, 0f, 180f);
for (int i = 0; i < n; i++)
{
// 줄 = 전체 + 등급별(등급 색 칩 = 범례 겸함) · 지금 고른 줄 = 오른쪽 ✔(테두리 효과는 틀 그림 외곽선이 겹쳐 검게 보임 · 3차 캡처 실측)
string key = i == 0 ? "" : grades[i - 1].Key;
var o = UiKit.Skin(UiKit.At(panel.transform, "O" + i, pad, pad + i * (rowH + gap), size.x - pad * 2f, rowH), "subtab", key.Length == 0 ? UiKit.Col("subtab_off", UiKit.Dark) : GradeColor(key), true);
o.gameObject.AddComponent().onClick.AddListener(() => SetGradeFilter(key));
UiKit.Title(UiKit.FillLabel(o.transform, key.Length == 0 ? "전체" : TreasureRules.GradeKo(key), fs, UiKit.White), true);
if (key == gradeFilter)
UiKit.Title(UiKit.Label(o.transform, "✔", fs, UiKit.White, TextAnchor.MiddleCenter, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-12f, 3f), new Vector2(rowH * 0.7f, rowH)), true);
}
}
/// 등급 고름("" = 전체) → 목록 접고 보유 중 다시 그림(테스트).
public void SetGradeFilter(string grade)
{
gradeFilter = grade ?? "";
CloseGradeFilter();
if (page == 0) Refresh();
}
public void CloseGradeFilter()
{
if (filterList == null) return;
filterList.gameObject.SetActive(false);
UnityEngine.Object.Destroy(filterList.gameObject);
filterList = null;
if (filterArrow != null) filterArrow.localEulerAngles = Vector3.zero;
}
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;
// 선반 진열 무대 팝업(PD 2026-11-13 2차 「예시 이미지와 같이 선택된 유물의 정보 화면에 선반 컨셉을 강조」) — 무대 그림이 없으면 예전 글 팝업
var stageRow = DomesticScene.First(TreasureScene.Scene("detail"), "stage");
var stageSp = stageRow != null ? UISkin.Icon(stageRow.sprite) : null;
if (stageSp != null)
{
DetailStage(it, st, stageRow, stageSp);
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기본 효과 ").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조건 효과 ").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= 지금 ").Append(TreasureRules.StatKo(it.baseStat)).Append(" +").Append(TreasureRules.Value(db, d, it).ToString("0.##")).Append("% ");
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강화 ★").Append(st.star + 1).Append(" 조각 ").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));
}
/// 정보 팝업이 무대 배치로 그려졌는가 · 무대 위 유물 아이콘(테스트).
public Image DetailRelic { get; private set; }
///
/// 유물 정보 = 선반 진열 무대(PD 2026-11-13 2차 · 예시 = 제목 → 등급 표시 → 선반 위 받침에 놓인 유물 → 보유 속성 표(★ 현재 → 다음) → 효과 상자) —
/// 표 detail_stage(무대 그림 · 방석 윗면 · 아이콘 크기 · 등급 목패 최대 폭) · detail_tag(3차 · 리본 폐기). 버튼 = 강화(또는 사유·최고 ★) · 닫기(+ 오른쪽 위 X · PD 「둘 다 남겨」).
///
void DetailStage(TreasureDb.Item it, TreasureState st, LobbyScene.Row stageRow, Sprite stageSp)
{
var db = S.Db;
var d = S.Save;
string id = it.id;
var rows = TreasureScene.Scene("detail");
var tagRow = DomesticScene.First(rows, "tag"); // 등급 목패(3차 · 리본 폐기)
var tagSp = tagRow != null ? UISkin.Icon(tagRow.sprite) : null;
bool maxed = st.star >= it.starMax;
var why = TreasureRules.CanEnhance(db, d, id);
int needShards = 0, needIron = 0;
if (!maxed) TreasureRules.EnhanceCost(db, it, st.star + 1, out needShards, out needIron);
string btn = maxed ? "최고 ★" : (why == null ? "강화 ★" + (st.star + 1) : why);
const float PanelH = 1320f;
var content = Root.ModalCustom(it.nameKo, PanelH,
new OutGameRoot.ModalButton(btn, why == null && !maxed ? 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));
const float W = 860f; // 내용 칸 폭(팝업 940 − 좌우 40)
float stageW = stageRow.w > 1f ? stageRow.w : W;
float k = stageW / Mathf.Max(1f, stageRow.h > 1f ? stageRow.h : stageSp.rect.width);
float stageH = stageSp.rect.height * stageW / Mathf.Max(1f, stageSp.rect.width);
// 무대 + 방석 위 유물
var stage = UiKit.At(content, "Stage", (W - stageW) * 0.5f, 26f, stageW, stageH);
var si = stage.gameObject.AddComponent();
si.sprite = stageSp;
si.raycastTarget = false;
UiKit.Border(si, UiKit.Line, 3f);
float iconSize = stageRow.min > 1f ? stageRow.min : 200f;
string icon = IconId(it);
DetailRelic = icon != null ? UiKit.Icon(stage, icon, new Vector2(0f, 1f), new Vector2(0.5f, 0f), new Vector2(stageRow.x * k, -stageRow.y * k - 6f), iconSize) : null;
if (DetailRelic != null) UiKit.DropShadow(DetailRelic, 6f);
// 등급 목패(무대 위 끝 가운데 · 등급 색 · 3차 리본 폐기) — 등급 · ★ · 폭 = 글자에 맞춤(표 detail_stage f_max 까지)
float rw = stageRow.max > 1f ? stageRow.max : 320f;
if (tagRow != null && tagSp != null)
{
NameTag(content, TreasureRules.GradeKo(it.grade) + " ★" + st.star + "/" + it.starMax, GradeColor(it.grade), UiKit.White, tagRow, tagSp, rw,
new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -26f - (tagRow.y > 0f ? tagRow.y : 0f))).transform.parent.name = "Grade";
}
else
{
var rib = UiKit.Box("Grade", content, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -26f), new Vector2(rw, 64f), GradeColor(it.grade));
UiKit.Title(UiKit.FillLabel(rib.transform, TreasureRules.GradeKo(it.grade) + " ★" + st.star + "/" + it.starMax, 30, UiKit.White), true);
}
// 보유 효과 제목(가는 줄 사이)
float y = 26f + stageH + 26f;
UiKit.Label(content, "보유 효과", 30, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -y), new Vector2(300f, 44f), false);
UiKit.Paint(UiKit.At(content, "LineL", 40f, y + 22f, 250f, 2f), UiKit.Col("text_soft", UiKit.InkSoft), false);
UiKit.Paint(UiKit.At(content, "LineR", W - 290f, y + 22f, 250f, 2f), UiKit.Col("text_soft", UiKit.InkSoft), false);
y += 58f;
// 강화 단계 표(머리 = 어두운 띠 · ★ 지금 → 다음)
var headBand = UiKit.Paint(UiKit.At(content, "TableHead", 0f, y, W, 60f), UiKit.Col("chip_dark", UiKit.Dark), false);
UiKit.Title(UiKit.Label(headBand.transform, "강화 단계", 30, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(24f, 0f), new Vector2(300f, 50f)), true);
UiKit.Title(UiKit.Label(headBand.transform, "★" + st.star, 30, UiKit.Col("star", UiKit.White), TextAnchor.MiddleCenter, new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(520f, 0f), new Vector2(160f, 50f)), true);
if (!maxed) UiKit.Title(UiKit.Label(headBand.transform, "★" + (st.star + 1), 30, UiKit.Col("star", UiKit.White), TextAnchor.MiddleCenter, new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(740f, 0f), new Vector2(160f, 50f)), true);
y += 60f;
var rowBand = UiKit.Paint(UiKit.At(content, "TableRow", 0f, y, W, 66f), UiKit.Col("chip", UiKit.Paper), false);
rowBand.color = new Color(rowBand.color.r, rowBand.color.g, rowBand.color.b, 0.55f);
UiKit.Label(rowBand.transform, TreasureRules.StatKo(it.baseStat), 30, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(24f, 0f), new Vector2(360f, 50f));
UiKit.Label(rowBand.transform, "+" + TreasureRules.BasePct(db, it, st.star).ToString("0.##") + "%", 32, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(520f, 0f), new Vector2(200f, 50f));
if (!maxed)
{
UiKit.Label(rowBand.transform, "≫", 30, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleCenter, new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(630f, 0f), new Vector2(60f, 50f));
UiKit.Label(rowBand.transform, "+" + TreasureRules.BasePct(db, it, st.star + 1).ToString("0.##") + "%", 32, UiKit.Ok, TextAnchor.MiddleCenter, new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(740f, 0f), new Vector2(200f, 50f));
}
y += 66f + 22f;
// 조건 효과 상자(머리 띠 + 내용)
int stacks = TreasureRules.CondStacks(d, it);
var condHead = UiKit.Paint(UiKit.At(content, "CondHead", 0f, y, W, 56f), UiKit.Col("chip_dark", UiKit.Dark), false);
UiKit.Title(UiKit.Label(condHead.transform, "조건 효과", 28, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(24f, 0f), new Vector2(400f, 50f)), true);
y += 56f;
var condBody = UiKit.Paint(UiKit.At(content, "CondBody", 0f, y, W, 150f), UiKit.Col("chip", UiKit.Paper), false);
condBody.color = new Color(condBody.color.r, condBody.color.g, condBody.color.b, 0.55f);
var cb = new System.Text.StringBuilder();
cb.Append("✦ ").Append(TreasureRules.CondKo(it.condKey)).Append(" ").Append(it.condEvery).Append("회마다 ").Append(TreasureRules.StatKo(it.baseStat)).Append(" +").Append(it.condPct.ToString("0.##")).Append("%")
.Append(" (").Append(stacks).Append("/").Append(it.condMax).Append(" · 누적 ").Append(TreasureRules.Counter(d, it.condKey)).Append(")");
cb.Append("\n✦ 지금 ").Append(TreasureRules.StatKo(it.baseStat)).Append(" +").Append(TreasureRules.Value(db, d, it).ToString("0.##")).Append("% ");
UiKit.FillLabel(condBody.transform, cb.ToString(), 28, UiKit.White, TextAnchor.MiddleLeft, false, 24f);
y += 150f + 18f;
// 세트 · 강화 재료
var tiers = it.setId.Length > 0 ? db.TreasureSets.TiersOf(it.setId) : null;
var foot = new List();
if (tiers != null && tiers.Count > 0) foot.Add("세트 「" + tiers[0].nameKo + "」");
if (!maxed) foot.Add("조각 " + st.shards + "/" + needShards + " · 운철 " + d.inv.iron + "/" + needIron);
if (foot.Count > 0)
UiKit.Label(content, string.Join(" ", foot.ToArray()), 28, UiKit.Col("text_soft", UiKit.InkSoft), TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -y), new Vector2(W, 44f), false);
}
// ---------- ② 세트 ----------
void BuildSets(RectTransform p)
{
var db = S.Db;
var d = S.Save;
ScrollRect sr;
// 보물고 진열장 배경 = 묶음 폭을 진열장 안쪽 판에 맞춤(표 vault_setgrid) · 아니면 예전 1032
var sg = VaultRow("setgrid");
int perRow = sg != null && sg.count > 0 ? sg.count : 5;
float mw = sg != null && sg.w > 1f ? sg.w : 196f, mh = sg != null && sg.h > 1f ? sg.h : 290f, mgap = sg != null && sg.gapMin > 0f ? sg.gapMin : 12f;
float bw = sg != null ? perRow * mw + (perRow - 1) * mgap : 1032f;
float pad = sg != null ? (OutGameRoot.Reference.x - bw) * 0.5f : 24f;
var list = UiKit.ScrollGrid("Sets", p, pad, pad, sg != null ? sg.y : 6f, 12f, new Vector2(bw, 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;
int owned = TreasureRules.OwnedIn(d, head);
// 보물고(붉은 벽) 위 = 흰 글자 + 외곽선(보유 중 「보유 n / m」 과 같은 모양 · 붉은 글자는 벽에 묻힘 · QA r1 M2)
bool vaultBg = ShownScene == "vault";
string ownTxt = "보유 " + owned + "/" + head.members.Count;
var plq = VaultRow("plaque");
var plqSp = plq != null ? UISkin.Icon(plq.sprite) : null;
float membersTop = 84f;
if (plqSp != null)
{
// 세트 제목 = 명패(PD 2026-11-13 3차 「세트 장식장의 제목을 명패처럼」 · 「보유 n/n 과 세트 제목이 겹치지 않게 충분한 공간」) — 표 vault_plaque:
// 오른쪽 끝 = 보유 글자 칸(f_x 폭 · 진열장 틀에서 f_gap_max 띄움) · 명패 = 가운데 · 폭 = 이름 + 좌우 장식(f_min)이되 양옆에 보유 칸 + 간격(f_gap_min)을 비운 폭까지
float ph = plq.h > 1f ? plq.h : 96f, side = plq.min > 1f ? plq.min : 66f;
float ownW = plq.x > 0f ? plq.x : 150f, gap = plq.gapMin > 0f ? plq.gapMin : 16f, inset = plq.gapMax > 0f ? plq.gapMax : 14f;
int pfs = plq.scale > 1f ? Mathf.RoundToInt(plq.scale) : 38;
float maxW = Mathf.Min(plq.w > 1f ? plq.w : 560f, bw - 2f * (inset + ownW + gap));
var plate = UiKit.Rect("Plaque", block.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), Vector2.zero, new Vector2(maxW, ph));
var pimg = plate.gameObject.AddComponent();
pimg.sprite = plqSp;
pimg.raycastTarget = false;
if (plqSp.border.sqrMagnitude > 0f) pimg.type = Image.Type.Sliced;
var pt = UiKit.Title(UiKit.FillLabel(plate, head.nameKo, pfs, UiKit.White, TextAnchor.MiddleCenter, true, side), true);
pt.horizontalOverflow = HorizontalWrapMode.Overflow;
float need = pt.preferredWidth;
if (need + side * 2f > maxW && need > 1f) pt.fontSize = Mathf.Max(24, Mathf.FloorToInt(pfs * (maxW - side * 2f) / need));
plate.sizeDelta = new Vector2(Mathf.Clamp(pt.preferredWidth + side * 2f, Mathf.Min(maxW, plq.max > 1f ? plq.max : ph * 3f), maxW), ph); // 가장 좁은 폭 = f_max
UiKit.Title(UiKit.Label(block.transform, ownTxt, 30, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(1f, 1f), new Vector2(1f, 0.5f), new Vector2(-inset, -ph * 0.5f), new Vector2(ownW, 50f)), true).name = "Owned";
membersTop = ph + (plq.y > 0f ? plq.y : 12f);
Plaques++;
}
else
{
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);
var ownedLbl = UiKit.Label(block.transform, ownTxt, 30, owned == head.members.Count || vaultBg ? UiKit.Ink : UiKit.Red, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(vaultBg ? -14f : 0f, -10f), new Vector2(200f, 50f));
if (vaultBg) UiKit.Title(ownedLbl, true); // 진열장 틀에서 14 px 띄움(QA r3 M8)
}
var row = UiKit.Rect("Members", block.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -membersTop), new Vector2(bw, mh));
var hl = row.gameObject.AddComponent();
hl.spacing = mgap;
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(mw, mh));
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(bw, 250f), UiKit.Paper);
UiKit.Border(fx, UiKit.Line, 2f);
var best = TreasureRules.SetTier(db, d, setId);
var sb = new System.Text.StringBuilder("기본 효과 (가장 높은 달성 단계 1개) ");
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 ? "" : "") : "").Append(label).Append(" ").Append(stat).Append(" +").Append(t.pct.ToString("0.#")).Append("%")
.Append(ok ? (t == best ? " ✔ 적용 " : " 달성") : " 미달성").Append(" ");
}
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();
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보유 " + count + " " : "열기\n상자 없음 ", 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(), 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×" + TreasureRules.ChestCount(d, ch.id) + " ", 26, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 14f), new Vector2(w, 100f));
}
}
///
/// 상자 탭(Fable 제안서 §4-5 · #1104) — 포인트 띠(BannerFrame05 · 가는 주황 바 · 「85 / 120」 · 오른쪽 보상 상자) →
/// 상자 카드(CardFrame03 상자 색 + 안쪽 + 광택 · 상자 아이콘 300 + 뒤 금빛 · 등급 칩 + % 한 줄 · 내용물 줄) · 좌우 화살표 →
/// 「열기」 Primary 노랑 + 보유 칩(없으면 회색) → 아래 상자 종류 행(ItemFrame 5층 상자 색 + 수량 칩 · 선택 = FocusLine 깜빡).
///
void BuildChestsKit(RectTransform p)
{
var db = S.Db;
var d = S.Save;
var soft = UiKit.Col("text_soft", UiKit.InkSoft);
PointsBand(p);
AutoPickChest();
var c = db.Chests.Get(SelectedChest);
if (c == null) return;
int count = TreasureRules.ChestCount(d, c.id);
BuildChestsKitBody(p, c, count, soft);
ChestRow(p, soft);
}
///
/// 상자 탭 = 금고(PD 2026-11-13 「상자 메뉴의 배경은 화려한 금고(주변에 금화가 쌓인) 속 올려진 상자를 여는 형태로 재구성」) —
/// 포인트 띠 → 금고 그림 받침대 방석 위에 상자(표 chest_spot · 뒤 금빛 맥동) · 위 이름 · 아래 확률 칩 · 내용물 줄 · 좌우 화살표 · 열기(누르면 상자가 흔들린 뒤 결과) · 아래 상자 종류 행.
/// 자리 = 표 Resources/UI/treasure_scene.csv(그림 px · ArtToPage 로 페이지 좌표).
///
void BuildChestsVault(RectTransform p, Scene s)
{
var db = S.Db;
var d = S.Save;
PointsBand(p);
AutoPickChest();
var c = db.Chests.Get(SelectedChest);
if (c == null) return;
int count = TreasureRules.ChestCount(d, c.id);
ChestOnPedestal = true;
var spot = DomesticScene.First(s.rows, "spot");
float sx = spot != null ? spot.x : s.artW * 0.5f, sy = spot != null ? spot.y : s.artH * 0.33f;
float size = spot != null && spot.w > 1f ? spot.w : 360f;
var at = ArtToPage(s, sx, sy);
System.Func rowY = (kind, def) =>
{
var r = DomesticScene.First(s.rows, kind);
return ArtToPage(s, sx, r != null ? r.y : def).y;
};
// 뒤 금빛 맥동 → 상자(받침대 방석 위)
if (UISkin.Has("glow_circle"))
{
var glow = UiKit.Skin(UiKit.Rect("Glow", p, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(at.x, -at.y), new Vector2(size * 1.7f, size * 1.7f)), "glow_circle", UiKit.Col("chest_glow", Color.clear));
var pulse = glow.gameObject.AddComponent();
pulse.min = 0.7f;
pulse.period = 1.6f;
}
openChestIcon = UiKit.Icon(p, "chest_" + c.id, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(at.x, -at.y), size);
if (openChestIcon != null) UiKit.DropShadow(openChestIcon, 8f);
// 이름 · 확률 칩 · 내용물 줄(금 바닥 위 = 외곽선 글자)
var name = UiKit.Title(UiKit.Label(p, c.nameKo, 56, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(at.x, -rowY("name", sy - 210f)), new Vector2(700f, 80f)), true);
name.horizontalOverflow = HorizontalWrapMode.Overflow;
var probs = new List();
for (int i = 0; i < ChestDb.ProbGrades.Length; i++)
if (c.p[i] > 0f) probs.Add(i);
const float Pw = 178f, Pg = 14f;
float py = rowY("probs", sy + 440f);
float px = at.x - (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(p, "P" + i, px + k * (Pw + Pg), py - 30f, 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();
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);
var eb = UiKit.Skin(UiKit.Rect("ExtraBand", p, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(at.x, -rowY("extra", sy + 500f)), new Vector2(720f, 52f)), "chip", UiKit.Col("chip_dark", UiKit.Dark));
var et = UiKit.Title(UiKit.FillLabel(eb.transform, string.Join(" · ", extra.ToArray()), 26, UiKit.White, TextAnchor.MiddleCenter, true, 16f), true);
et.horizontalOverflow = HorizontalWrapMode.Overflow;
// 좌우 화살표(상자 높이)
var ar = DomesticScene.First(s.rows, "arrow");
float ax = ar != null ? ar.x : 30f;
float ay = rowY("arrow", sy);
Arrow(p, "Prev", "arrow_prev", ax, ay - 70f, () => Step(-1));
Arrow(p, "Next", "arrow_next", 1080f - ax - 100f, ay - 70f, () => Step(1));
// 열기(받침대 위 상자가 흔들린 뒤 결과) · 안내
float oy = rowY("open", sy + 540f);
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, oy, 460f, 116f, () => OpenWithShake(SelectedChest));
open.interactable = count > 0;
if (count > 0) UiKit.BtnCost(open, "chest_" + c.id, "x" + count, 56f, 34);
var note = UiKit.Title(UiKit.Label(p, "상자는 이 탭에서만 엽니다 · 중복 = 조각 · 천기 = 적장 토벌 전용", 26, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, -rowY("note", sy + 690f)), new Vector2(1000f, 44f), false), true);
note.horizontalOverflow = HorizontalWrapMode.Overflow;
ChestRow(p, UiKit.White);
}
/// 열기 버튼과 같은 동작(받침대 배치면 흔든 뒤 열기 · 아니면 바로) — 테스트·캡처용 공개 진입점.
public void OpenAnimated(string id)
{
OpenWithShake(id);
}
/// 받침대 위 상자를 흔든 뒤(0.4 초 · 실제 시간) 연다 — 상자가 없으면 바로 DoOpen(토스트).
void OpenWithShake(string id)
{
if (openChestIcon == null || Root == null || !Root.isActiveAndEnabled)
{
DoOpen(id);
return;
}
Root.StartCoroutine(ShakeThenOpen(openChestIcon.rectTransform, id));
}
System.Collections.IEnumerator ShakeThenOpen(RectTransform rt, string id)
{
const float Dur = 0.4f;
float t = 0f;
while (t < Dur && rt != null)
{
t += Time.unscaledDeltaTime;
float k = t / Dur;
rt.localRotation = Quaternion.Euler(0f, 0f, Mathf.Sin(k * Mathf.PI * 8f) * 7f * (1f - k));
rt.localScale = Vector3.one * (1f + 0.12f * Mathf.Sin(k * Mathf.PI));
yield return null;
}
if (rt != null)
{
rt.localRotation = Quaternion.identity;
rt.localScale = Vector3.one;
}
DoOpen(id);
}
/// 상자 포인트 띠(맨 위 · 「n포인트 더 모으면 명장 상자」 · 바 · 「85 / 120」 · 오른쪽 보상 상자).
void PointsBand(RectTransform p)
{
var db = S.Db;
var d = S.Save;
var e = db.Economy;
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);
}
}
/// 처음 연 상자 탭에서 고른 상자가 0개면 가진 상자 중 첫 번째로.
void AutoPickChest()
{
var db = S.Db;
var d = S.Save;
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;
}
}
/// 예전 상자 탭 몸통(무대 문양 + 카드 + 화살표 + 열기) — 금고 배경 그림이 없을 때.
void BuildChestsKitBody(RectTransform p, ChestDb.Chest c, int count, Color soft)
{
// 무대 = 카드 뒤 문양 흰 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();
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();
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();
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);
}
/// 상자 종류 행(아래 · ItemFrame 5층 상자 색 + 수량 칩 · 선택 = FocusLine 깜빡) — 예전 배치·금고 배치 공통.
void ChestRow(RectTransform p, Color soft)
{
var db = S.Db;
var d = S.Save;
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();
hit.color = Color.clear;
cell.gameObject.AddComponent().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);
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();
// 1.08 배는 아래 가운데 기준(위·양옆으로만 커짐 = 아래끝·칩이 이름 쪽으로 내려오지 않음 · QA r1 B2 · r2 B3)
var pos = cell.anchoredPosition;
cell.pivot = new Vector2(0.5f, 0f);
cell.anchoredPosition = pos + new Vector2(Tw * 0.5f, -Tw);
cell.localScale = Vector3.one * 1.08f;
}
else UiKit.Layer(cell, "Dim", "item_bg", UiKit.Col("chest_dim", new Color(0f, 0f, 0f, 0.25f)));
// 수량 칩 = 선택 테두리 위(테두리가 칩을 덮지 않게 · QA r1 B2)
UiKit.QtyChip(cell, "x" + TreasureRules.ChestCount(d, ch.id), 28);
// 이름 = 모든 칸 같은 높이(선택 칸은 위로만 커짐 · 이름을 내리면 솟은 하단 「보물」 탭 뒤로 잘림 = QA r2 B3) · 금고 배경 = 외곽선(QA r1 M1)
float nameY = ty + Tw + 10f;
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, -nameY), new Vector2(Tw + gap, 36f), sel);
nl.horizontalOverflow = HorizontalWrapMode.Overflow;
if (ShownScene == "chest") UiKit.Title(nl, true);
}
}
/// 화살표 버튼(투명 판 + 팩 화살표 아이콘 · 없으면 글자).
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();
img.color = Color.clear;
rt.gameObject.AddComponent().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("").Append(it.nameKo).Append(" \n").Append(TreasureRules.GradeKo(it.grade)).Append(r.gradeUp ? " (상위 등급!)" : "")
.Append(r.isNew ? " NEW " : " 조각 +" + r.shards).Append("\n\n");
var parts = new List();
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포인트 달성 → ").Append(S.Db.Chests.Get(S.Db.Economy.ChestPointReward).nameKo).Append(" +").Append(r.bonusChests).Append(" ");
Root.Modal(S.Db.Chests.Get(id).nameKo + " 열기", sb.ToString(),
new OutGameRoot.ModalButton("한 번 더", UiKit.Red, () => DoOpen(id)),
new OutGameRoot.ModalButton("확인", UiKit.Dark, null));
}
/// 상자 대표 색 = 테마 chest_<id>(구 코드 상수 → ui_theme.csv · C45 · #1104).
public static Color ChestColor(string id)
{
return UiKit.Col("chest_" + id, UiKit.Slot);
}
}
}