diff --git a/Assets/Scripts/OutGame/UI/HeroScreens.cs b/Assets/Scripts/OutGame/UI/HeroScreens.cs
index c18c43c2..95d7ca63 100644
--- a/Assets/Scripts/OutGame/UI/HeroScreens.cs
+++ b/Assets/Scripts/OutGame/UI/HeroScreens.cs
@@ -47,10 +47,11 @@ namespace TKS.OutGame
name.horizontalOverflow = HorizontalWrapMode.Overflow;
// 위 줄: 병종 원 · Lv 배지 · 세력 깃발 · (출전 꼬리 칩)
- var ring = UiKit.Skin(UiKit.Rect("Type", t, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(142f, -12f), new Vector2(52f, 52f)), "dot", UiKit.White);
- ring.preserveAspect = true;
- var cls = UiKit.Icon(ring.transform, UiKit.TypeIconId(h.type), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 36f);
- if (cls == null) UiKit.Title(UiKit.FillLabel(ring.transform, UnitTypeInfo.Mark(h.type), 26, UiKit.White), true);
+ // 병과 = 단색 심볼만(배경 틀 없음 · PD 2026-11-09)
+ var ring = UiKit.Rect("Type", t, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(142f, -12f), new Vector2(52f, 52f));
+ var cls = UiKit.Icon(ring, UiKit.TypeSymbolId(h.type), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 50f);
+ if (cls != null) UiKit.DropShadow(cls, 2f);
+ else UiKit.Title(UiKit.FillLabel(ring, UnitTypeInfo.Mark(h.type), 26, UiKit.White), true);
if (owned)
{
var badge = UiKit.Icon(t, "lv_badge_s", new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(202f, -8f), 56f);
@@ -58,8 +59,7 @@ namespace TKS.OutGame
badge != null ? new Vector2(0.5f, 0.5f) : new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), badge != null ? new Vector2(0f, 2f) : new Vector2(230f, -36f), new Vector2(56f, 40f)), true);
lt.horizontalOverflow = HorizontalWrapMode.Overflow;
}
- var flag = UiKit.Icon(t, UISkin.Icon("flag_" + h.faction) != null ? "flag_" + h.faction : "flag_other", new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(264f, -14f), 44f);
- UiKit.DropShadow(flag, 2f);
+ UiKit.DropShadow(UiKit.FactionBadge(t, h.faction, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(264f, -12f), 46f), 2f); // 진영 원 배지(PD 2026-11-09)
if (deployed)
{
var tail = UiKit.Skin(UiKit.Rect("Deployed", t, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-10f, -10f), new Vector2(104f, 46f)), "tail", UiKit.Col("tail_red", UiKit.Red));
@@ -87,6 +87,12 @@ namespace TKS.OutGame
/// 크기 표 size_px(머리 크기 기준 표준 키)가 카드에서 차지할 px — 배율 = UnitPx ÷ size_px(모든 유닛 머리 크기 같음) · 머리 끝 위치(카드 위에서 px).
const float UnitPx = 360f; // 450 → 360 = 유닛 20 % 작게(PD 2026-11-09 「현재 비율보다 20 % 정도 작게」)
const float HeadTop = 50f;
+ /// 걷는 유닛(보병·창병·궁병·책사) = 머리 끝을 더 내려 발이 카드 아래로 숨게(PD 2026-11-09).
+ const float FootDrop = 36f;
+ /// 말 탄 유닛(크기 표 mounted) = 더 작게 · 말 머리가 카드 안에 보이게(PD 2026-11-09).
+ const float MountedScale = 0.72f;
+ /// 말 탄 유닛 전신(발 ~ 머리 끝) 높이 상한(카드 px) — 크기 표 실측이 튀는 그림(예 여포 size_px 449)도 다른 기병과 비슷하게.
+ const float MountedMaxBody = 390f;
public static Image Card(Transform content, OutGameSession s, HeroDb.Hero h, bool deployed, System.Action onTap)
{
@@ -118,7 +124,11 @@ namespace TKS.OutGame
{
float hgt = sp.rect.height;
float headY = Mathf.Clamp01((m.pivot[1] * hgt + body) / hgt); // 머리 끝 = 프레임 아래에서의 비율
- art = UiKit.Rect("Character", b, new Vector2(0.5f, 1f), new Vector2(0.5f, headY), new Vector2(0f, -HeadTop), sp.rect.size * (UnitPx / size)).gameObject.AddComponent();
+ bool mounted = a.Mounted;
+ float k = UnitPx / size * (mounted ? MountedScale : 1f);
+ if (mounted) k = Mathf.Min(k, MountedMaxBody / body);
+ float top = HeadTop + (mounted ? 0f : FootDrop);
+ art = UiKit.Rect("Character", b, new Vector2(0.5f, 1f), new Vector2(0.5f, headY), new Vector2(0f, -top), sp.rect.size * k).gameObject.AddComponent();
}
else
{
@@ -165,10 +175,11 @@ namespace TKS.OutGame
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 role = UiKit.Skin(UiKit.Rect("Role", t, new Vector2(1f, 1f), new Vector2(0.5f, 0.5f), new Vector2(-42f, -42f), new Vector2(58f, 60f)), "dot", UiKit.Col("circle_inner", UiKit.Dark));
- role.preserveAspect = true;
- if (UiKit.Icon(role.transform, UiKit.TypeSymbolId(h.type), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 46f) == null) // 병과 심볼(PD 2026-11-09)
- UiKit.Title(UiKit.FillLabel(role.transform, UnitTypeInfo.Mark(h.type), 28, 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.DropShadow(sym, 3f);
+ else 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); // 성장 가능 = 레벨 배지 오른쪽 위
return cell;
}
@@ -360,7 +371,7 @@ namespace TKS.OutGame
Image promoteImg;
Text promoteSub;
Image bandImg;
- Image flagIcon;
+ RectTransform factionRoot;
Image clsIcon;
Image paraImg;
Text gradeText;
@@ -576,10 +587,8 @@ namespace TKS.OutGame
// ④ 이름 띠 = 어두운 띠 + 세력 깃발 · 병종 원 · 등급 칩 · 이름 · 병종 · ★
bandImg = Spr(body, "NameBand", "card", 24f, 776f, 1032f, 114f, "white");
Spr(body, "BandLight", "band_light", 40f, 782f, 1000f, 12f, "band_light");
- flagIcon = Ico(body, null, 44f, 789f, 88f, 88f, 3f);
- Spr(body, "TypeRing", "circle_frame", 140f, 794f, 78f, 78f, "white");
- Spr(body, "TypeIn", "circle_frame", 144f, 798f, 70f, 70f, "circle_inner");
- clsIcon = Ico(body, null, 147f, 801f, 64f, 64f);
+ factionRoot = UiKit.At(body, "Faction", 44f, 789f, 88f, 88f); // 진영 원 배지 자리(PD 2026-11-09 · 구 세력 깃발)
+ clsIcon = Ico(body, null, 140f, 794f, 78f, 78f, 3f); // 병과 = 단색 심볼만(배경 틀 없음 · PD 2026-11-09)
paraImg = Spr(body, "Grade", "para_green", 232f, 805f, 96f, 56f, "white");
gradeText = Txt(body, "", 232f, 811f, 38, "white", TextAnchor.MiddleCenter, 96f);
nameText = Txt(body, "", 344f, 797f, 66, "white");
@@ -733,8 +742,9 @@ namespace TKS.OutGame
evolveDot.gameObject.SetActive(!h.isSoldier && hs != null && evolveDot.sprite != null && HeroRules.CanEvolve(db, h, hs) == null);
// 이름 띠
- SetIcon(flagIcon, UISkin.Icon("flag_" + h.faction) != null ? "flag_" + h.faction : "flag_other", 1f);
- SetIcon(clsIcon, UiKit.TypeIconId(h.type), 1f);
+ UiKit.Clear(factionRoot);
+ UiKit.DropShadow(UiKit.FactionBadge(factionRoot, h.faction, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 84f), 3f);
+ SetIcon(clsIcon, UiKit.TypeSymbolId(h.type), 1f);
nameText.text = h.nameKo;
typeText.text = UnitTypeInfo.Korean(h.type);
PlaceX(typeText, 344f + nameText.preferredWidth + 24f);
@@ -750,7 +760,7 @@ namespace TKS.OutGame
var sk = SkillRules.Skill(db, h, i);
bool has = sk != null && !h.isSoldier && hs != null;
SetIcon(skFrame[i], tk != null ? "skill_" + tk + (i == 0 ? "_a" : "_p") : null, has ? 1f : 0.5f);
- SetIcon(skCls[i], UiKit.TypeIconId(h.type), has ? 1f : 0.5f);
+ SetIcon(skCls[i], UiKit.TypeSymbolId(h.type), has ? 1f : 0.5f);
skLv[i].transform.parent.gameObject.SetActive(has);
skBtn[i].gameObject.SetActive(has);
if (i == 0) coolChip.SetActive(has);
diff --git a/Assets/Scripts/OutGame/UI/OutGameRoot.cs b/Assets/Scripts/OutGame/UI/OutGameRoot.cs
index 93ce0364..8ae3e2b4 100644
--- a/Assets/Scripts/OutGame/UI/OutGameRoot.cs
+++ b/Assets/Scripts/OutGame/UI/OutGameRoot.cs
@@ -791,13 +791,9 @@ namespace TKS.OutGame
{
var ut = (UnitType)kinds[k];
float cx = ex + k * Ew + 44f;
- var circ = UiKit.Skin(UiKit.Rect("Type" + k, field, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(cx, -y - 94f), new Vector2(80f, 80f)), "circle_frame", UiKit.TypeColor(ut));
- var ti = UiKit.Icon(circ.transform, UiKit.TypeIconId(ut), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 2f), 52f);
- if (ti != null)
- {
- ti.color = UiKit.White;
- UiKit.DropShadow(ti, 2f);
- }
+ var circ = UiKit.Rect("Type" + k, field, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), new Vector2(cx, -y - 94f), new Vector2(80f, 80f));
+ var ti = UiKit.Icon(circ, UiKit.TypeSymbolId(ut), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 76f); // 병과 = 단색 심볼만(배경 틀 없음 · PD 2026-11-09)
+ if (ti != null) UiKit.DropShadow(ti, 2f);
var n = UiKit.Title(UiKit.Label(field, "x" + counts[kinds[k]], 36, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(cx + 50f, -y - 94f), new Vector2(100f, 50f)), true);
n.horizontalOverflow = HorizontalWrapMode.Overflow;
}
diff --git a/Assets/Scripts/OutGame/UI/UiKit.cs b/Assets/Scripts/OutGame/UI/UiKit.cs
index 86707ffe..edbb03c1 100644
--- a/Assets/Scripts/OutGame/UI/UiKit.cs
+++ b/Assets/Scripts/OutGame/UI/UiKit.cs
@@ -828,9 +828,9 @@ namespace TKS.OutGame
/// 병종 배지(병종 색 칩 + 글자 · 신규 제작 cls_* 아이콘이 오면 아이콘).
public static Image TypeBadge(Transform parent, UnitType t, Vector2 anchor, Vector2 pivot, Vector2 pos, float h)
{
- var b = Box("Type", parent, anchor, pivot, pos, new Vector2(h * 1.3f, h), UISkin.On ? TypeColor(t) : Dark);
- string icon = TypeIconId(t);
- if (HasFinalIcon(icon)) Icon(b.transform, icon, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 1f), h * 0.8f);
+ var b = Box("Type", parent, anchor, pivot, pos, new Vector2(h * 1.3f, h), UISkin.On ? Color.clear : Dark); // 테마 = 틀 없이 단색 심볼(PD 2026-11-09)
+ string icon = TypeSymbolId(t);
+ if (UISkin.Icon(icon) != null) Icon(b.transform, icon, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 1f), h * 0.8f);
else Title(FillLabel(b.transform, UnitTypeInfo.Mark(t), Mathf.RoundToInt(h * 0.62f), White), true);
return b;
}
diff --git a/Assets/Scripts/Runtime/BattleHUD.cs b/Assets/Scripts/Runtime/BattleHUD.cs
index b023b1f4..fe06eaf7 100644
--- a/Assets/Scripts/Runtime/BattleHUD.cs
+++ b/Assets/Scripts/Runtime/BattleHUD.cs
@@ -219,8 +219,11 @@ namespace TKS
mask.gameObject.AddComponent().showMaskGraphic = false;
portraitArt = MakeImage("Art", mask.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -40f), new Vector2(230f, 230f), Color.white);
portraitArt.preserveAspect = true;
+ // 병과 = 단색 심볼만(배경 틀 없음 · PD 2026-11-09)
typeRing = OutGame.UiKit.Skin(OutGame.UiKit.At(unitBody, "TypeRing", 188f, 20f, 52f, 52f), "circle_frame", Color.white);
- typeIcon = OutGame.UiKit.Icon(typeRing.transform, "cls_infantry", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 34f);
+ typeRing.enabled = false;
+ typeIcon = OutGame.UiKit.Icon(typeRing.transform, OutGame.UiKit.TypeSymbolId(UnitType.Infantry), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, 52f);
+ if (typeIcon != null) OutGame.UiKit.DropShadow(typeIcon, 2f);
nameText = OutGame.UiKit.Title(OutGame.UiKit.Label(unitBody, "", 36, Color.white, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(250f, -18f), new Vector2(300f, 56f)), true);
nameText.horizontalOverflow = HorizontalWrapMode.Overflow;
stateText = OutGame.UiKit.Label(unitBody, "", 24, soft, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-20f, -22f), new Vector2(220f, 48f), false);
@@ -288,8 +291,7 @@ namespace TKS
portraitArt.sprite = sp;
portraitArt.enabled = sp != null;
portraitRing.color = u.Faction == Faction.Player ? UISkin.Col("hud_turn_player", Color.blue) : UISkin.Col("hud_turn_enemy", Color.red);
- typeRing.color = OutGame.UiKit.TypeColor(u.Type);
- var tsp = UISkin.Icon(OutGame.UiKit.TypeIconId(u.Type));
+ var tsp = UISkin.Icon(OutGame.UiKit.TypeSymbolId(u.Type));
if (typeIcon != null && tsp != null) typeIcon.sprite = tsp;
}
nameText.text = u.DisplayName + (u.IsBoss ? " 장수" : "");
diff --git a/Assets/UI/Symbols/cls_sym_archer.png b/Assets/UI/Symbols/cls_sym_archer.png
index f5b6d71d..24a2850e 100644
Binary files a/Assets/UI/Symbols/cls_sym_archer.png and b/Assets/UI/Symbols/cls_sym_archer.png differ
diff --git a/Assets/UI/Symbols/cls_sym_cavalry.png b/Assets/UI/Symbols/cls_sym_cavalry.png
index 4c96e8d0..4bdfa2a6 100644
Binary files a/Assets/UI/Symbols/cls_sym_cavalry.png and b/Assets/UI/Symbols/cls_sym_cavalry.png differ
diff --git a/Assets/UI/Symbols/cls_sym_infantry.png b/Assets/UI/Symbols/cls_sym_infantry.png
index 02449bfe..3a60fc20 100644
Binary files a/Assets/UI/Symbols/cls_sym_infantry.png and b/Assets/UI/Symbols/cls_sym_infantry.png differ
diff --git a/Assets/UI/Symbols/cls_sym_spear.png b/Assets/UI/Symbols/cls_sym_spear.png
index dc40b226..9f09f44d 100644
Binary files a/Assets/UI/Symbols/cls_sym_spear.png and b/Assets/UI/Symbols/cls_sym_spear.png differ
diff --git a/Assets/UI/Symbols/cls_sym_strategist.png b/Assets/UI/Symbols/cls_sym_strategist.png
index 7eb659be..384ab06c 100644
Binary files a/Assets/UI/Symbols/cls_sym_strategist.png and b/Assets/UI/Symbols/cls_sym_strategist.png differ
diff --git a/Tools/class_symbols.py b/Tools/class_symbols.py
index c66b727d..0eb75e2a 100644
--- a/Tools/class_symbols.py
+++ b/Tools/class_symbols.py
@@ -2,7 +2,7 @@
새로 그리지 않는다 — 확정 납품 메달 아이콘(Assets/UI/Samguk/class/cls_<병과>.png)을 만들 때 쓴 투명 바탕 사물 그림
(아트 R&D `C:/ArtRnD/samguk_ui/_work/class_symbol/rgba/cls_<병과>_s21A_rgba.png` · picked_auto.json = seed 21)의 투명도 = 형태.
-형태를 채움색으로 칠하고, 원본의 진한 안쪽 선(눈 · 칼날 경계 등)만 선 색으로 남긴 뒤 바깥 테두리를 두른다.
+기본 = 단색(PD 2026-11-09 「배경 프레임 없이 단색」 · 채움 한 색 · 원본의 진한 안쪽 선 = 뚫린 선) · --outline = 테두리 + 안쪽 선 색(구 방식).
실행: -I -B Tools/class_symbols.py [--src ] [--fill #RRGGBB] [--line #RRGGBB] [--out Assets/UI/Symbols] [--preview ]
출력 = /cls_sym_<병과>.png(256 정사각 · 투명) · --preview = 어두운 원 위 5종 확인 그림.
"""
@@ -22,7 +22,7 @@ def hexrgb(s):
return tuple(int(s[i:i + 2], 16) for i in (0, 2, 4))
-def symbol(path, fill, line, size=256, pad=14, stroke=8, inner_lum=45, erode_passes=6):
+def symbol(path, fill, line, size=256, pad=14, stroke=8, inner_lum=45, erode_passes=6, mono=True):
src = Image.open(path).convert("RGBA")
bb = src.getchannel("A").point(lambda v: 255 if v > 40 else 0).getbbox()
src = src.crop(bb)
@@ -42,6 +42,12 @@ def symbol(path, fill, line, size=256, pad=14, stroke=8, inner_lum=45, erode_pas
canvas.paste(alpha, off)
lines = Image.new("L", (size, size), 0)
lines.paste(inner, off)
+ if mono:
+ # 단색(PD 2026-11-09 「배경 프레임 없이 단색」) = 채움 한 색 · 테두리 없음 · 안쪽 선 = 뚫린 선(형태 구분)
+ out = Image.new("RGBA", (size, size), fill + (255,))
+ a = np.asarray(canvas).astype(np.float32) * (1.0 - np.asarray(lines).astype(np.float32) / 255.0)
+ out.putalpha(Image.fromarray(a.clip(0, 255).astype(np.uint8)))
+ return out
out = Image.new("RGBA", (size, size), line + (0,))
out.putalpha(canvas.filter(ImageFilter.MaxFilter(stroke * 2 + 1))) # 바깥 테두리
body = Image.new("RGBA", (size, size), fill + (255,))
@@ -61,15 +67,15 @@ def main():
ap.add_argument("--line", default="#2A2230")
ap.add_argument("--out", default="Assets/UI/Symbols")
ap.add_argument("--preview", default="")
+ ap.add_argument("--outline", action="store_true", help="테두리 + 진한 안쪽 선(구 방식) · 기본 = 단색")
a = ap.parse_args()
out = os.path.join(ROOT, a.out)
os.makedirs(out, exist_ok=True)
tiles = []
for t in TYPES:
- g = symbol(os.path.join(a.src, "cls_%s_%sA_rgba.png" % (t, a.seed)), hexrgb(a.fill), hexrgb(a.line))
+ g = symbol(os.path.join(a.src, "cls_%s_%sA_rgba.png" % (t, a.seed)), hexrgb(a.fill), hexrgb(a.line), mono=not a.outline)
g.save(os.path.join(out, "cls_sym_%s.png" % t))
tile = Image.new("RGBA", (300, 300), (0, 0, 0, 0))
- ImageDraw.Draw(tile).ellipse((10, 10, 290, 290), fill=(53, 55, 78, 255), outline=(20, 20, 30, 255), width=8)
tile.alpha_composite(g.resize((200, 200), Image.LANCZOS), (50, 50))
tiles.append(tile)
if a.preview: