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: