diff --git a/Assets/Data/UI/ui_theme.csv b/Assets/Data/UI/ui_theme.csv index 544d9f53..0683e9cb 100644 --- a/Assets/Data/UI/ui_theme.csv +++ b/Assets/Data/UI/ui_theme.csv @@ -304,6 +304,8 @@ color,card_empty,,,,,,#303146,빈·미보유 카드(팩 CardFrame01_Empty Bg 원 color,card_empty_pattern,,,,,,#2D2F42,빈·미보유 카드 문양 color,card_empty_border,,,,,,#1A1B27,빈·미보유 카드 테두리 color,card_dim,,,,,,#000000A0,미보유 초상 어둡게 +color,card_locked_art,,,,,,#A9AEC2,영웅 목록 미보유 카드 일러 틴트(모습은 보이게 · 보유보다 조금 어둡게 · PD 2026-11-10) +color,tail_locked,,,,,,#4A4C63,영웅 목록 미보유 꼬리 칩 color,tail_red,,,,,,#D8412F,출전 꼬리 칩 color,grade_white_card_bg,,,,,,#626E8B,영웅 카드 white card_bg color,grade_white_card_grad,,,,,,#90A3BA,영웅 카드 white card_grad diff --git a/Assets/Resources/UI/UITheme.asset b/Assets/Resources/UI/UITheme.asset index 198d10f6..b1803dd7 100644 --- a/Assets/Resources/UI/UITheme.asset +++ b/Assets/Resources/UI/UITheme.asset @@ -335,6 +335,10 @@ MonoBehaviour: color: {r: 0.101960786, g: 0.105882354, b: 0.15294118, a: 1} - id: card_dim color: {r: 0, g: 0, b: 0, a: 0.627451} + - id: card_locked_art + color: {r: 0.6627451, g: 0.68235296, b: 0.7607843, a: 1} + - id: tail_locked + color: {r: 0.2901961, g: 0.29803923, b: 0.3882353, a: 1} - id: tail_red color: {r: 0.84705883, g: 0.25490198, b: 0.18431373, a: 1} - id: grade_white_card_bg diff --git a/Assets/Scripts/OutGame/UI/HeroScreens.cs b/Assets/Scripts/OutGame/UI/HeroScreens.cs index 300c0b95..d713f62c 100644 --- a/Assets/Scripts/OutGame/UI/HeroScreens.cs +++ b/Assets/Scripts/OutGame/UI/HeroScreens.cs @@ -95,7 +95,8 @@ namespace TKS.OutGame /// 말 탄 유닛 전신(발 ~ 머리 끝) 높이 상한(카드 px) — 크기 표 실측이 튀는 그림(예 여포 size_px 449)도 다른 기병과 비슷하게. const float MountedMaxBody = 460f; - public static Image Card(Transform content, OutGameSession s, HeroDb.Hero h, bool deployed, System.Action onTap) + /// false = 미보유(PD 2026-11-10 「유닛 목록에 장수들의 모습이 보이도록」) — 일러를 테마 card_locked_art 로 조금 어둡게 · 「미보유」 꼬리 칩 · 레벨 배지·성장 점 없음 + public static Image Card(Transform content, OutGameSession s, HeroDb.Hero h, bool deployed, System.Action onTap, bool owned = true) { var cell = UiKit.Cell(content, "Card_" + h.id, Color.clear, onTap); cell.sprite = null; // 바닥 = 클릭 판(투명) · 그림은 층으로 @@ -138,6 +139,7 @@ namespace TKS.OutGame } art.sprite = sp; art.raycastTarget = false; + if (!owned) art.color = UiKit.Col("card_locked_art", UiKit.InkSoft); } else { @@ -155,10 +157,10 @@ namespace TKS.OutGame int star = hs != null ? hs.star : h.rarity; for (int i = star - 1; i >= 0; i--) UiKit.Icon(t, "star_on", new Vector2(1f, 0f), new Vector2(1f, 1f), new Vector2(-15.5f - i * 28f, 64.8f), 44f); - // 레벨 배지(왼쪽 아래 · 최고 레벨 = 노랑) + // 레벨 배지(왼쪽 아래 · 최고 레벨 = 노랑 · 미보유 = 없음) int lvn = lv != null ? lv.lv : 1; bool maxLv = s.Db.Levels != null && lvn >= s.Db.Levels.MaxLevel; - var badge = UiKit.Icon(t, maxLv ? "lv_badge_max" : "lv_badge_s", new Vector2(0f, 0f), new Vector2(0.5f, 0.5f), new Vector2(33.9f, 36.7f), 79f); + var badge = owned ? UiKit.Icon(t, maxLv ? "lv_badge_max" : "lv_badge_s", new Vector2(0f, 0f), new Vector2(0.5f, 0.5f), new Vector2(33.9f, 36.7f), 79f) : null; if (badge != null) { var bt = UiKit.Title(UiKit.Label(badge.transform, lvn.ToString(), 30, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(1f, -3f), new Vector2(74f, 50f)), true); @@ -171,23 +173,31 @@ namespace TKS.OutGame name.resizeTextForBestFit = true; name.resizeTextMinSize = 26; name.resizeTextMaxSize = 45; - if (deployed) + if (deployed || !owned) { - var tail = UiKit.Skin(UiKit.Rect("Deployed", t, new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(10f, -96f), new Vector2(84f, 38f)), "tail", UiKit.Col("tail_red", UiKit.Red)); - UiKit.Title(UiKit.FillLabel(tail.transform, "출전", 24, UiKit.White), true); + var tail = UiKit.Skin(UiKit.Rect(owned ? "Deployed" : "Locked", t, new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(10f, -96f), new Vector2(owned ? 84f : 104f, 38f)), "tail", + owned ? UiKit.Col("tail_red", UiKit.Red) : UiKit.Col("tail_locked", UiKit.Dark)); + UiKit.Title(UiKit.FillLabel(tail.transform, owned ? "출전" : "미보유", 24, UiKit.White), true); } // 병과 = 단색 심볼만(배경 틀 없음 · PD 2026-11-09 「심볼은 배경 프레임 없이 단색」) var role = UiKit.Rect("Role", t, new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), new Vector2(-42f, -42f), new Vector2(60f, 60f)); var sym = UiKit.Icon(role, UiKit.TypeSymbolId(h.type), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 60f); if (sym == null) UiKit.Title(UiKit.FillLabel(role, UnitTypeInfo.Mark(h.type), 28, UiKit.White), true); - if (s.CanGrowAny(h.id)) UiKit.Icon(t, "new_dot", new Vector2(0f, 0f), new Vector2(0.5f, 0.5f), new Vector2(64f, 70f), 30f); // 성장 가능 = 레벨 배지 오른쪽 위 + if (owned && s.CanGrowAny(h.id)) UiKit.Icon(t, "new_dot", new Vector2(0f, 0f), new Vector2(0.5f, 0.5f), new Vector2(64f, 70f), 30f); // 성장 가능 = 레벨 배지 오른쪽 위 return cell; } - /// 원래 크기(320×431) 카드를 holder 가운데에 배율 k 로 넣는다(편성 자리 · 편성 보유 목록). - public static Image CardIn(Transform holder, OutGameSession s, HeroDb.Hero h, bool deployed, System.Action onTap, float k) + /// 원래 크기(320×431) 카드를 holder 가운데에 배율 k 로 넣는다(편성 자리 · 편성 보유 목록 · 영웅 목록 미보유 = owned false). + public static Image CardIn(Transform holder, OutGameSession s, HeroDb.Hero h, bool deployed, System.Action onTap, float k, bool owned = true) { - return Fit(Card(holder, s, h, deployed, onTap), k); + return Fit(Card(holder, s, h, deployed, onTap, owned), k); + } + + /// 미보유·도감 안내 한 줄(이름(한자) · 병종 · ★ · 보유/미보유). + public static string InfoText(HeroDb.Hero h, bool own) + { + return h.nameKo + (string.IsNullOrEmpty(h.hanja) ? "" : "(" + h.hanja + ")") + " · " + UnitTypeInfo.Korean(h.type) + " · ★" + h.rarity + + (h.isSoldier ? " · 병사 유닛" : "") + (own ? " · 보유" : " · 미보유 — 거래 > 뽑기"); } public static Image EmptyIn(Transform holder, float k) @@ -227,7 +237,7 @@ namespace TKS.OutGame /// /// 영웅 탭 = 영웅 목록(PD 2026-11-09 레퍼런스 = 팩 Collection_List) — 헤더(제목 · 장수/병사 고르기 ▼ · 사선 칸 「보유 / 전체」) · - /// 3열 세로 카드(보유 = 레벨 높은 순) + 미보유 자리 = 빈 카드 → 탭 = 영웅 상세. + /// 세로 카드(보유 = 레벨 높은 순 → 탭 = 영웅 상세) + 미보유 = 그 영웅 카드(조금 어둡게 · 「미보유」 · 탭 = 획득 안내 · PD 2026-11-10). /// public class HeroListScreen : OutGameScreen { @@ -237,6 +247,7 @@ namespace TKS.OutGame Text filterText; Text countText; int ownedCards; + int lockedCards; UiAutoGrid autoGrid; public bool ShowingSoldiers @@ -315,19 +326,36 @@ namespace TKS.OutGame CollectionCards.CardIn(Slot(), S, h, S.InActivePreset(id), () => Root.ShowHero(id), autoGrid.Scale); ownedCards++; } - int total = 0; - foreach (var h in S.Db.Heroes.heroes) - if (h.isSoldier == soldiers) total++; - for (int i = owned.Count; i < total; i++) CollectionCards.EmptyIn(Slot(), autoGrid.Scale); - countText.text = owned.Count + "/" + total; + // 미보유 = 빈 카드 대신 그 영웅 카드(일러 조금 어둡게 · 「미보유」 · 탭 = 획득 안내) · ★ 높은 순 → 표 순서 + // PD 2026-11-10 「왜 유닛 목록에 장수들의 모습이 보이지 않는거지? 이번에 추가된 리소스가 반영되도록」 + // (상세 화면은 레벨·세이브가 있는 보유 영웅 전제라 미보유는 열지 않는다) + var locked = new List(); + var all = S.Db.Heroes.heroes; + foreach (var h in all) + if (h.isSoldier == soldiers && !S.Save.Owns(h.id)) locked.Add(h); + locked.Sort((a, b) => a.rarity != b.rarity ? b.rarity.CompareTo(a.rarity) : all.IndexOf(a).CompareTo(all.IndexOf(b))); + lockedCards = 0; + foreach (var h in locked) + { + var hero = h; + CollectionCards.CardIn(Slot(), S, h, false, () => Root.Toast(CollectionCards.InfoText(hero, false)), autoGrid.Scale, false); + lockedCards++; + } + countText.text = owned.Count + "/" + (owned.Count + locked.Count); autoGrid.Apply(); } - /// 보유 카드 수(빈 카드 제외). + /// 보유 카드 수(미보유 제외). public int CardCount { get { return ownedCards; } } + + /// 미보유 영웅 카드 수(그림 카드 · PD 2026-11-10). + public int LockedCount + { + get { return lockedCards; } + } } /// @@ -968,8 +996,7 @@ namespace TKS.OutGame { bool own = s.Save.Owns(h.id); var hero = h; - HeroCards.Card(grid, s, h, false, own, () => root.Toast(hero.nameKo + (string.IsNullOrEmpty(hero.hanja) ? "" : "(" + hero.hanja + ")") + " · " + - UnitTypeInfo.Korean(hero.type) + " · ★" + hero.rarity + (hero.isSoldier ? " · 병사 유닛" : "") + (own ? " · 보유" : " · 미보유 — 거래 > 뽑기"))); + HeroCards.Card(grid, s, h, false, own, () => root.Toast(CollectionCards.InfoText(hero, own))); } } } diff --git a/Assets/Tests/PlayMode/OutGameTests.cs b/Assets/Tests/PlayMode/OutGameTests.cs index 863adbd7..92f9fbac 100644 --- a/Assets/Tests/PlayMode/OutGameTests.cs +++ b/Assets/Tests/PlayMode/OutGameTests.cs @@ -479,6 +479,17 @@ namespace TKS.Tests Assert.AreEqual(og.HeroList, og.Current); yield return null; Assert.AreEqual(og.Session.Owned(false).Count, og.HeroList.CardCount, "보유 장수 카드"); + // 미보유 장수 = 빈 카드가 아니라 그 영웅 그림 카드(PD 2026-11-10 「유닛 목록에 장수들의 모습이 보이도록」) + int heroTotal = 0; + foreach (var h in og.Session.Db.Heroes.heroes) + if (!h.isSoldier) heroTotal++; + Assert.AreEqual(heroTotal - og.Session.Owned(false).Count, og.HeroList.LockedCount, "미보유 장수 카드 = 그림 카드"); + Assert.Greater(og.HeroList.LockedCount, 0, "신규 유저 = 미보유 있음"); + var lockedTail = og.HeroList.Rt.GetComponentsInChildren(true); + int lockedWithArt = 0; + foreach (var tr in lockedTail) + if (tr.name == "Locked" && tr.parent.Find("Bg/Character") != null) lockedWithArt++; + Assert.AreEqual(og.HeroList.LockedCount, lockedWithArt, "미보유 카드마다 일러(Character)"); og.HeroList.SetFilter(true); yield return null; Assert.AreEqual(og.Session.Owned(true).Count, og.HeroList.CardCount, "병사 필터");