영웅 목록 = 미보유 영웅도 그림 카드(빈 카드 대신 · 일러 조금 어둡게 · 「미보유」 칩 · 탭 = 획득 안내)(#1108 · PD 2026-11-10 「왜 유닛 목록에 장수들의 모습이 보이지 않는거지? 이번에 추가된 리소스가 반영되도록」)

- HeroScreens: CollectionCards.Card owned 인자 · 미보유 = 레벨 배지·성장 점 없음 · ★ 높은 순 → 표 순서 · 상세 화면은 보유 전제라 미보유 탭 = 토스트(도감과 같은 안내 InfoText)
- 테마 표 ui_theme.csv: card_locked_art · tail_locked(SOT) → TKS/Import UI Theme
- 테스트(고친 부분만): Lobby_Tabs_Back_ComingSoon 에 미보유 카드 수·일러 확인 추가 1/1 · UILayerTests 1/1 · error 0
- 캡처 hero_list 1장 · 배치 134(미보유 64장 그림이 각자 다른 텍스처 → 축소 초상 아틀라스 대상)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-10 10:49:27 +09:00
parent cd6e84ec1e
commit 9718b99a00
4 changed files with 63 additions and 19 deletions

View File

@ -304,6 +304,8 @@ color,card_empty,,,,,,#303146,빈·미보유 카드(팩 CardFrame01_Empty Bg 원
color,card_empty_pattern,,,,,,#2D2F42,빈·미보유 카드 문양 color,card_empty_pattern,,,,,,#2D2F42,빈·미보유 카드 문양
color,card_empty_border,,,,,,#1A1B27,빈·미보유 카드 테두리 color,card_empty_border,,,,,,#1A1B27,빈·미보유 카드 테두리
color,card_dim,,,,,,#000000A0,미보유 초상 어둡게 color,card_dim,,,,,,#000000A0,미보유 초상 어둡게
color,card_locked_art,,,,,,#A9AEC2,영웅 목록 미보유 카드 일러 틴트(모습은 보이게 · 보유보다 조금 어둡게 · PD 2026-11-10)
color,tail_locked,,,,,,#4A4C63,영웅 목록 미보유 꼬리 칩
color,tail_red,,,,,,#D8412F,출전 꼬리 칩 color,tail_red,,,,,,#D8412F,출전 꼬리 칩
color,grade_white_card_bg,,,,,,#626E8B,영웅 카드 white card_bg color,grade_white_card_bg,,,,,,#626E8B,영웅 카드 white card_bg
color,grade_white_card_grad,,,,,,#90A3BA,영웅 카드 white card_grad color,grade_white_card_grad,,,,,,#90A3BA,영웅 카드 white card_grad

1 kind id path overlay ppu inset alpha color note
304 color card_empty_pattern #2D2F42 빈·미보유 카드 문양
305 color card_empty_border #1A1B27 빈·미보유 카드 테두리
306 color card_dim #000000A0 미보유 초상 어둡게
307 color card_locked_art #A9AEC2 영웅 목록 미보유 카드 일러 틴트(모습은 보이게 · 보유보다 조금 어둡게 · PD 2026-11-10)
308 color tail_locked #4A4C63 영웅 목록 미보유 꼬리 칩
309 color tail_red #D8412F 출전 꼬리 칩
310 color grade_white_card_bg #626E8B 영웅 카드 white card_bg
311 color grade_white_card_grad #90A3BA 영웅 카드 white card_grad

View File

@ -335,6 +335,10 @@ MonoBehaviour:
color: {r: 0.101960786, g: 0.105882354, b: 0.15294118, a: 1} color: {r: 0.101960786, g: 0.105882354, b: 0.15294118, a: 1}
- id: card_dim - id: card_dim
color: {r: 0, g: 0, b: 0, a: 0.627451} 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 - id: tail_red
color: {r: 0.84705883, g: 0.25490198, b: 0.18431373, a: 1} color: {r: 0.84705883, g: 0.25490198, b: 0.18431373, a: 1}
- id: grade_white_card_bg - id: grade_white_card_bg

View File

@ -95,7 +95,8 @@ namespace TKS.OutGame
/// <summary>말 탄 유닛 전신(발 ~ 머리 끝) 높이 상한(카드 px) — 크기 표 실측이 튀는 그림(예 여포 size_px 449)도 다른 기병과 비슷하게.</summary> /// <summary>말 탄 유닛 전신(발 ~ 머리 끝) 높이 상한(카드 px) — 크기 표 실측이 튀는 그림(예 여포 size_px 449)도 다른 기병과 비슷하게.</summary>
const float MountedMaxBody = 460f; const float MountedMaxBody = 460f;
public static Image Card(Transform content, OutGameSession s, HeroDb.Hero h, bool deployed, System.Action onTap) /// <param name="owned">false = 미보유(PD 2026-11-10 「유닛 목록에 장수들의 모습이 보이도록」) — 일러를 테마 card_locked_art 로 조금 어둡게 · 「미보유」 꼬리 칩 · 레벨 배지·성장 점 없음</param>
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); var cell = UiKit.Cell(content, "Card_" + h.id, Color.clear, onTap);
cell.sprite = null; // 바닥 = 클릭 판(투명) · 그림은 층으로 cell.sprite = null; // 바닥 = 클릭 판(투명) · 그림은 층으로
@ -138,6 +139,7 @@ namespace TKS.OutGame
} }
art.sprite = sp; art.sprite = sp;
art.raycastTarget = false; art.raycastTarget = false;
if (!owned) art.color = UiKit.Col("card_locked_art", UiKit.InkSoft);
} }
else else
{ {
@ -155,10 +157,10 @@ namespace TKS.OutGame
int star = hs != null ? hs.star : h.rarity; int star = hs != null ? hs.star : h.rarity;
for (int i = star - 1; i >= 0; i--) 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); 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; int lvn = lv != null ? lv.lv : 1;
bool maxLv = s.Db.Levels != null && lvn >= s.Db.Levels.MaxLevel; 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) 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); 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.resizeTextForBestFit = true;
name.resizeTextMinSize = 26; name.resizeTextMinSize = 26;
name.resizeTextMaxSize = 45; 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)); 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",
UiKit.Title(UiKit.FillLabel(tail.transform, "출전", 24, UiKit.White), true); 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 「심볼은 배경 프레임 없이 단색」) // 병과 = 단색 심볼만(배경 틀 없음 · 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 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); 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 (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; return cell;
} }
/// <summary>원래 크기(320×431) 카드를 holder 가운데에 배율 k 로 넣는다(편성 자리 · 편성 보유 목록).</summary> /// <summary>원래 크기(320×431) 카드를 holder 가운데에 배율 k 로 넣는다(편성 자리 · 편성 보유 목록 · 영웅 목록 미보유 = owned false).</summary>
public static Image CardIn(Transform holder, OutGameSession s, HeroDb.Hero h, bool deployed, System.Action onTap, float k) 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);
}
/// <summary>미보유·도감 안내 한 줄(이름(한자) · 병종 · ★ · 보유/미보유).</summary>
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) public static Image EmptyIn(Transform holder, float k)
@ -227,7 +237,7 @@ namespace TKS.OutGame
/// <summary> /// <summary>
/// 영웅 탭 = 영웅 목록(PD 2026-11-09 레퍼런스 = 팩 Collection_List) — 헤더(제목 · 장수/병사 고르기 ▼ · 사선 칸 「보유 / 전체」) · /// 영웅 탭 = 영웅 목록(PD 2026-11-09 레퍼런스 = 팩 Collection_List) — 헤더(제목 · 장수/병사 고르기 ▼ · 사선 칸 「보유 / 전체」) ·
/// 3열 세로 카드(보유 = 레벨 높은 순) + 미보유 자리 = 빈 카드 → 탭 = 영웅 상세. /// 세로 카드(보유 = 레벨 높은 순 → 탭 = 영웅 상세) + 미보유 = 그 영웅 카드(조금 어둡게 · 「미보유」 · 탭 = 획득 안내 · PD 2026-11-10).
/// </summary> /// </summary>
public class HeroListScreen : OutGameScreen public class HeroListScreen : OutGameScreen
{ {
@ -237,6 +247,7 @@ namespace TKS.OutGame
Text filterText; Text filterText;
Text countText; Text countText;
int ownedCards; int ownedCards;
int lockedCards;
UiAutoGrid autoGrid; UiAutoGrid autoGrid;
public bool ShowingSoldiers public bool ShowingSoldiers
@ -315,19 +326,36 @@ namespace TKS.OutGame
CollectionCards.CardIn(Slot(), S, h, S.InActivePreset(id), () => Root.ShowHero(id), autoGrid.Scale); CollectionCards.CardIn(Slot(), S, h, S.InActivePreset(id), () => Root.ShowHero(id), autoGrid.Scale);
ownedCards++; ownedCards++;
} }
int total = 0; // 미보유 = 빈 카드 대신 그 영웅 카드(일러 조금 어둡게 · 「미보유」 · 탭 = 획득 안내) · ★ 높은 순 → 표 순서
foreach (var h in S.Db.Heroes.heroes) // PD 2026-11-10 「왜 유닛 목록에 장수들의 모습이 보이지 않는거지? 이번에 추가된 리소스가 반영되도록」
if (h.isSoldier == soldiers) total++; // (상세 화면은 레벨·세이브가 있는 보유 영웅 전제라 미보유는 열지 않는다)
for (int i = owned.Count; i < total; i++) CollectionCards.EmptyIn(Slot(), autoGrid.Scale); var locked = new List<HeroDb.Hero>();
countText.text = owned.Count + "/" + total; 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(); autoGrid.Apply();
} }
/// <summary>보유 카드 수(빈 카드 제외).</summary> /// <summary>보유 카드 수(미보유 제외).</summary>
public int CardCount public int CardCount
{ {
get { return ownedCards; } get { return ownedCards; }
} }
/// <summary>미보유 영웅 카드 수(그림 카드 · PD 2026-11-10).</summary>
public int LockedCount
{
get { return lockedCards; }
}
} }
/// <summary> /// <summary>
@ -968,8 +996,7 @@ namespace TKS.OutGame
{ {
bool own = s.Save.Owns(h.id); bool own = s.Save.Owns(h.id);
var hero = h; var hero = h;
HeroCards.Card(grid, s, h, false, own, () => root.Toast(hero.nameKo + (string.IsNullOrEmpty(hero.hanja) ? "" : "(" + hero.hanja + ")") + " · " + HeroCards.Card(grid, s, h, false, own, () => root.Toast(CollectionCards.InfoText(hero, own)));
UnitTypeInfo.Korean(hero.type) + " · ★" + hero.rarity + (hero.isSoldier ? " · 병사 유닛" : "") + (own ? " · 보유" : " · 미보유 — 거래 > 뽑기")));
} }
} }
} }

View File

@ -479,6 +479,17 @@ namespace TKS.Tests
Assert.AreEqual(og.HeroList, og.Current); Assert.AreEqual(og.HeroList, og.Current);
yield return null; yield return null;
Assert.AreEqual(og.Session.Owned(false).Count, og.HeroList.CardCount, "보유 장수 카드"); 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<Transform>(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); og.HeroList.SetFilter(true);
yield return null; yield return null;
Assert.AreEqual(og.Session.Owned(true).Count, og.HeroList.CardCount, "병사 필터"); Assert.AreEqual(og.Session.Owned(true).Count, og.HeroList.CardCount, "병사 필터");