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) ----------