diff --git a/Assets/Resources/BalanceConfig.asset b/Assets/Resources/BalanceConfig.asset
index c784f80f..ea265fcf 100644
--- a/Assets/Resources/BalanceConfig.asset
+++ b/Assets/Resources/BalanceConfig.asset
@@ -92,6 +92,7 @@ MonoBehaviour:
collisionPushRatio: 1
collisionSelfBounceRatio: 0.5
unitArtHeight: 1.5
+ mountedArtMaxTop: 1.3
boardArt: battlefield_01
minDragDistance: 0.35
finalHitTolerance: 0.15
diff --git a/Assets/Resources/Units/guanyu/front_idle/f000.png b/Assets/Resources/Units/guanyu/front_idle/f000.png
index e1b61e21..cfb351cc 100644
Binary files a/Assets/Resources/Units/guanyu/front_idle/f000.png and b/Assets/Resources/Units/guanyu/front_idle/f000.png differ
diff --git a/Assets/Scripts/Editor/BalanceDefaults.cs b/Assets/Scripts/Editor/BalanceDefaults.cs
index 9f770194..4e15223b 100644
--- a/Assets/Scripts/Editor/BalanceDefaults.cs
+++ b/Assets/Scripts/Editor/BalanceDefaults.cs
@@ -56,6 +56,7 @@ namespace TKS.EditorTools
c.collisionSelfBounceRatio = 0.5f; // PD 2026-11-04 #1081: 보병 자신도 반대로 튕김(남은 거리의 절반 · 손맛 조정 대상)
c.spearRecoilRatio = 0.5f; // PD 2026-11-06 #1098: 창병 밀치기 반작용(밀린 거리의 절반 · 함께 전진 폐기)
c.unitArtHeight = 1.5f;
+ c.mountedArtMaxTop = 1.3f; // 기마 머리 끝 상한 = 표준 키 × 1.3(PD 2026-11-09)
c.boardArt = "battlefield_01"; // 원화 세션 1차 납품(2026-11-04) · Resources/Board/
c.minDragDistance = 0.35f;
c.finalHitTolerance = 0.15f;
diff --git a/Assets/Scripts/Runtime/BalanceConfig.cs b/Assets/Scripts/Runtime/BalanceConfig.cs
index 57d60d48..cb0421b3 100644
--- a/Assets/Scripts/Runtime/BalanceConfig.cs
+++ b/Assets/Scripts/Runtime/BalanceConfig.cs
@@ -152,6 +152,8 @@ namespace TKS
[Header("표시")]
[Tooltip("유닛 아트(스프라이트) 세로 크기 · 월드 단위 · 장수는 반지름 배율만큼 커진다")]
public float unitArtHeight = 1.5f;
+ [Tooltip("말 탄 유닛(Resources/Units/art_size.csv mounted = 1)의 그려진 머리 끝 상한 = 표준 키 × 이 값 — 크기 표대로면 탄 사람 = 보병 크기라 1.4~1.8배로 커짐 → PD 2026-11-09 「완벽한 비율 아니어도 됨 · 보병보다 조금 크게 · 너무 커지지 않게」 · 0 = 상한 없음")]
+ public float mountedArtMaxTop = 1.3f;
[Tooltip("Resources/Board/<이름> 전장 배경 스프라이트(원화 세션 납품 · 5:8) · 비면 도형·격자 바닥")]
public string boardArt = "battlefield_01";
diff --git a/Assets/Scripts/Runtime/Unit.cs b/Assets/Scripts/Runtime/Unit.cs
index 4890a5b3..21d9b28a 100644
--- a/Assets/Scripts/Runtime/Unit.cs
+++ b/Assets/Scripts/Runtime/Unit.cs
@@ -25,6 +25,8 @@ namespace TKS
public string ArtId = "";
/// 아트 머리 끝 높이(월드) — Init = 표준 키(unitArtHeight × 반지름 배율) · 아트가 있으면 UnitView 가 크기 표 배율 뒤 실제 머리 끝으로 바꾼다.
public float ArtHeight;
+ /// 말 탄 유닛 머리 끝 상한 배율(BalanceConfig.mountedArtMaxTop · 표준 키 기준).
+ public float MountedArtMaxTop;
/// 이번 아군 턴에 행동을 마쳤는가.
public bool ActedThisTurn;
@@ -62,6 +64,7 @@ namespace TKS
float radiusMult = boss ? cfg.bossRadiusMult : 1f;
Radius = s.radius * radiusMult;
ArtHeight = cfg.unitArtHeight * radiusMult;
+ MountedArtMaxTop = cfg.mountedArtMaxTop;
SlingMaxDistance = s.slingMaxDistance;
// PD 2026-11-04 #1080: 궁병 사거리 = 슬링샷 범위(제자리 사격) · 그 밖 +비율 안의 적은 들어올 만큼만 다가가 사격 · 창병은 자산의 대기 반경
// 책사(PD 2026-11-05 #1087 ⑤) = 궁병과 같은 사거리 규칙
diff --git a/Assets/Scripts/Runtime/UnitArt.cs b/Assets/Scripts/Runtime/UnitArt.cs
index 44071f7d..6b6ca687 100644
--- a/Assets/Scripts/Runtime/UnitArt.cs
+++ b/Assets/Scripts/Runtime/UnitArt.cs
@@ -42,6 +42,8 @@ namespace TKS
/// 0 = 표에 없음 → BodyPx. PD 2026-11-09 「캐릭터 크기 동일 · 말 탄 유닛도 보병·창병과 비슷한 비율」.
///
public float SizePx;
+ /// 말 탄 유닛(크기 표 mounted = 1) — 게임 배치 때 머리 끝 상한(BalanceConfig.mountedArtMaxTop)을 받는다.
+ public bool Mounted;
/// 그림 속 몸(머리 끝~발) 세로 px — bodyHeightPx(무기 제외) 우선, 없으면 charHeightPx.
public float BodyPx
@@ -68,36 +70,48 @@ namespace TKS
public const string SizeTable = "art_size";
static readonly Dictionary cache = new Dictionary();
static Dictionary sizes;
+ static HashSet mounted;
/// 크기 표의 size_px(없으면 0).
public static float SizeOf(string id)
{
- if (sizes == null) sizes = ParseSizes(Resources.Load(Root + SizeTable));
+ EnsureSizes();
float v;
return id != null && sizes.TryGetValue(id, out v) ? v : 0f;
}
- static Dictionary ParseSizes(TextAsset ta)
+ /// 크기 표 mounted = 1(말 탄 유닛).
+ public static bool IsMounted(string id)
{
- var d = new Dictionary();
- if (ta == null) return d;
+ EnsureSizes();
+ return id != null && mounted.Contains(id);
+ }
+
+ static void EnsureSizes()
+ {
+ if (sizes != null) return;
+ sizes = new Dictionary();
+ mounted = new HashSet();
+ var ta = Resources.Load(Root + SizeTable);
+ if (ta == null) return;
var lines = ta.text.TrimStart('').Split('\n');
- if (lines.Length == 0) return d;
var head = new List(lines[0].Trim().Split(','));
- int ia = head.IndexOf("art"), isz = head.IndexOf("size_px");
- if (ia < 0 || isz < 0)
+ int ia = head.IndexOf("art"), isz = head.IndexOf("size_px"), im = head.IndexOf("mounted");
+ if (ia < 0 || isz < 0 || im < 0)
{
- Debug.LogWarning("[TKS] 유닛 크기 표 열(art · size_px) 없음");
- return d;
+ Debug.LogWarning("[TKS] 유닛 크기 표 열(art · mounted · size_px) 없음");
+ return;
}
+ int need = Mathf.Max(ia, Mathf.Max(isz, im));
for (int i = 1; i < lines.Length; i++)
{
var c = lines[i].Trim().Split(',');
+ if (c.Length <= need) continue;
float v;
- if (c.Length > Mathf.Max(ia, isz) && float.TryParse(c[isz], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v) && v > 0f)
- d[c[ia]] = v;
+ if (float.TryParse(c[isz], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v) && v > 0f)
+ sizes[c[ia]] = v;
+ if (c[im] == "1") mounted.Add(c[ia]);
}
- return d;
}
public static UnitArtData Load(string id)
@@ -114,6 +128,7 @@ namespace TKS
{
cache.Clear();
sizes = null;
+ mounted = null;
}
static UnitArtData LoadUncached(string id)
@@ -132,7 +147,7 @@ namespace TKS
}
if (meta == null || meta.clips == null) return null;
- var d = new UnitArtData { Id = id, Meta = meta, SizePx = SizeOf(id) };
+ var d = new UnitArtData { Id = id, Meta = meta, SizePx = SizeOf(id), Mounted = IsMounted(id) };
foreach (var c in meta.clips)
{
if (c == null || string.IsNullOrEmpty(c.name)) continue;
@@ -183,7 +198,8 @@ namespace TKS
public float TopHeight { get; private set; }
/// 표준 키(월드) — 크기 표 size_px 가 이 높이가 되도록 배율(= 머리 크기 통일).
- public void Setup(UnitArtData d, float worldHeight, int sortingOrder)
+ /// 말 탄 유닛의 머리 끝 상한 = worldHeight × 이 값(0 = 상한 없음 · 넘으면 배율을 줄임 = 탄 사람은 보병보다 조금 작아짐).
+ public void Setup(UnitArtData d, float worldHeight, int sortingOrder, float mountedMaxTop = 0f)
{
data = d;
sr = GetComponent();
@@ -191,8 +207,14 @@ namespace TKS
sr.sortingOrder = sortingOrder;
float h = d.CharHeightWorld;
float s = h > 0.001f ? worldHeight / h : 1f;
+ float top = d.Meta.ppu > 0.01f ? d.BodyPx / d.Meta.ppu * s : worldHeight;
+ if (d.Mounted && mountedMaxTop > 0.01f && top > worldHeight * mountedMaxTop)
+ {
+ s *= worldHeight * mountedMaxTop / top;
+ top = worldHeight * mountedMaxTop;
+ }
transform.localScale = new Vector3(s, s, 1f);
- TopHeight = d.Meta.ppu > 0.01f ? d.BodyPx / d.Meta.ppu * s : worldHeight;
+ TopHeight = top;
}
public bool Has(string clip)
diff --git a/Assets/Scripts/Runtime/UnitView.cs b/Assets/Scripts/Runtime/UnitView.cs
index 6e02c904..07b601b7 100644
--- a/Assets/Scripts/Runtime/UnitView.cs
+++ b/Assets/Scripts/Runtime/UnitView.cs
@@ -139,7 +139,7 @@ namespace TKS
ago.transform.SetParent(transform, false);
ago.transform.localPosition = new Vector3(0f, -u.Radius * 0.55f, 0f);
anim = ago.AddComponent();
- anim.Setup(art, u.ArtHeight, 0);
+ anim.Setup(art, u.ArtHeight, 0, u.MountedArtMaxTop);
u.ArtHeight = anim.TopHeight; // 크기 표 배율 뒤 실제 머리 끝(HP 바 · 머리 위 표시 기준)
anim.Renderer.flipX = facingLeft;
anim.SetIdle(ClipName("idle"));
diff --git a/Assets/Tests/PlayMode/BattleFlowTests.cs b/Assets/Tests/PlayMode/BattleFlowTests.cs
index 6466caa0..5fc683e8 100644
--- a/Assets/Tests/PlayMode/BattleFlowTests.cs
+++ b/Assets/Tests/PlayMode/BattleFlowTests.cs
@@ -1015,7 +1015,13 @@ namespace TKS.Tests
var u = bm.SpawnUnit(UnitType.Cavalry, Faction.Player, new Vector2(0f, -4f), "조운", 1f, 1f, false, "zhaoyun");
yield return null;
Assert.AreEqual(u.View.Anim.TopHeight, u.ArtHeight, 1e-4f, "ArtHeight = 그려진 머리 끝");
- Assert.Greater(u.ArtHeight, cfg.unitArtHeight, "기마 = 표준 키보다 높음");
+ // PD 2026-11-09 「완벽한 비율 아니어도 됨 · 보병보다 조금 크게 · 너무 커지지 않게」 = 머리 끝 상한(표준 키 × mountedArtMaxTop)
+ Assert.Greater(cfg.mountedArtMaxTop, 1f, "기마 상한 > 표준 키");
+ Assert.AreEqual(cfg.unitArtHeight * cfg.mountedArtMaxTop, u.ArtHeight, 1e-3f, "기마 머리 끝 = 표준 키 × 상한(조운 원래 ×1.56 → 상한)");
+ var foot = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(2f, -4f), "유비", 1f, 1f, false, "liubei");
+ yield return null;
+ Assert.AreEqual(foot.View.Anim.TopHeight, foot.ArtHeight, 1e-4f, "보병 = 상한 무관(크기 표 그대로)");
+ Assert.Less(foot.ArtHeight, u.ArtHeight, "기마가 보병보다 조금 큼");
}
// ---------- 스테이지 표(Assets/Data/Stages/*.csv · 145 스테이지 · 2026-11-05 #1086) ----------