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, "병사 필터");