기마 크기 상한 ×1.3 · 관우 대기 그림 귀 조각 덮음(#1104 · PD 2026-11-09)

- PD 「완벽한 비율 아니어도 됨(게임적 허용) · 기마는 보병·창병보다 조금 크게 · 너무 커지지 않게 조금 줄여」:
  BalanceConfig.mountedArtMaxTop = 1.3(크기 표 mounted = 1 의 그려진 머리 끝 상한 = 표준 키 × 1.3) →
  조운·조조군 기병 ×1.56 · 여포 ×1.82 · 장비 ×1.43 · 황건 기병 ×1.38 → 모두 ×1.3(탄 사람 = 보병의 약 0.8~0.9)
  · 영웅 목록 카드는 그대로(머리 크기 같음)
- PD 「관우 옆얼굴이 깨진 것처럼 보임」: 원인 = front_idle 오른쪽 귀(붉은 조각 3,795 px)가 외곽선 없이 머리카락 사이에 끼어 있음
  → 그 조각만 둘레 머리카락으로 메움(cv2.inpaint TELEA · 검은 외곽선·얼굴 유지 · 재생성 없음) · ready·action 은 귀에 외곽선이 있어 그대로
- 테스트 118/118(기마 머리 끝 = 표준 키 × 상한 · 보병 < 기마) · console error 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-09 19:33:00 +09:00
parent c64be33646
commit b27edb3c49
8 changed files with 52 additions and 17 deletions

View File

@ -92,6 +92,7 @@ MonoBehaviour:
collisionPushRatio: 1 collisionPushRatio: 1
collisionSelfBounceRatio: 0.5 collisionSelfBounceRatio: 0.5
unitArtHeight: 1.5 unitArtHeight: 1.5
mountedArtMaxTop: 1.3
boardArt: battlefield_01 boardArt: battlefield_01
minDragDistance: 0.35 minDragDistance: 0.35
finalHitTolerance: 0.15 finalHitTolerance: 0.15

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 376 KiB

View File

@ -56,6 +56,7 @@ namespace TKS.EditorTools
c.collisionSelfBounceRatio = 0.5f; // PD 2026-11-04 #1081: 보병 자신도 반대로 튕김(남은 거리의 절반 · 손맛 조정 대상) c.collisionSelfBounceRatio = 0.5f; // PD 2026-11-04 #1081: 보병 자신도 반대로 튕김(남은 거리의 절반 · 손맛 조정 대상)
c.spearRecoilRatio = 0.5f; // PD 2026-11-06 #1098: 창병 밀치기 반작용(밀린 거리의 절반 · 함께 전진 폐기) c.spearRecoilRatio = 0.5f; // PD 2026-11-06 #1098: 창병 밀치기 반작용(밀린 거리의 절반 · 함께 전진 폐기)
c.unitArtHeight = 1.5f; 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.boardArt = "battlefield_01"; // 원화 세션 1차 납품(2026-11-04) · Resources/Board/
c.minDragDistance = 0.35f; c.minDragDistance = 0.35f;
c.finalHitTolerance = 0.15f; c.finalHitTolerance = 0.15f;

View File

@ -152,6 +152,8 @@ namespace TKS
[Header("표시")] [Header("표시")]
[Tooltip("유닛 아트(스프라이트) 세로 크기 · 월드 단위 · 장수는 반지름 배율만큼 커진다")] [Tooltip("유닛 아트(스프라이트) 세로 크기 · 월드 단위 · 장수는 반지름 배율만큼 커진다")]
public float unitArtHeight = 1.5f; 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) · 비면 도형·격자 바닥")] [Tooltip("Resources/Board/<이름> 전장 배경 스프라이트(원화 세션 납품 · 5:8) · 비면 도형·격자 바닥")]
public string boardArt = "battlefield_01"; public string boardArt = "battlefield_01";

View File

@ -25,6 +25,8 @@ namespace TKS
public string ArtId = ""; public string ArtId = "";
/// <summary>아트 머리 끝 높이(월드) — Init = 표준 키(unitArtHeight × 반지름 배율) · 아트가 있으면 UnitView 가 크기 표 배율 뒤 실제 머리 끝으로 바꾼다.</summary> /// <summary>아트 머리 끝 높이(월드) — Init = 표준 키(unitArtHeight × 반지름 배율) · 아트가 있으면 UnitView 가 크기 표 배율 뒤 실제 머리 끝으로 바꾼다.</summary>
public float ArtHeight; public float ArtHeight;
/// <summary>말 탄 유닛 머리 끝 상한 배율(BalanceConfig.mountedArtMaxTop · 표준 키 기준).</summary>
public float MountedArtMaxTop;
/// <summary>이번 아군 턴에 행동을 마쳤는가.</summary> /// <summary>이번 아군 턴에 행동을 마쳤는가.</summary>
public bool ActedThisTurn; public bool ActedThisTurn;
@ -62,6 +64,7 @@ namespace TKS
float radiusMult = boss ? cfg.bossRadiusMult : 1f; float radiusMult = boss ? cfg.bossRadiusMult : 1f;
Radius = s.radius * radiusMult; Radius = s.radius * radiusMult;
ArtHeight = cfg.unitArtHeight * radiusMult; ArtHeight = cfg.unitArtHeight * radiusMult;
MountedArtMaxTop = cfg.mountedArtMaxTop;
SlingMaxDistance = s.slingMaxDistance; SlingMaxDistance = s.slingMaxDistance;
// PD 2026-11-04 #1080: 궁병 사거리 = 슬링샷 범위(제자리 사격) · 그 밖 +비율 안의 적은 들어올 만큼만 다가가 사격 · 창병은 자산의 대기 반경 // PD 2026-11-04 #1080: 궁병 사거리 = 슬링샷 범위(제자리 사격) · 그 밖 +비율 안의 적은 들어올 만큼만 다가가 사격 · 창병은 자산의 대기 반경
// 책사(PD 2026-11-05 #1087 ⑤) = 궁병과 같은 사거리 규칙 // 책사(PD 2026-11-05 #1087 ⑤) = 궁병과 같은 사거리 규칙

View File

@ -42,6 +42,8 @@ namespace TKS
/// 0 = 표에 없음 → BodyPx. PD 2026-11-09 「캐릭터 크기 동일 · 말 탄 유닛도 보병·창병과 비슷한 비율」. /// 0 = 표에 없음 → BodyPx. PD 2026-11-09 「캐릭터 크기 동일 · 말 탄 유닛도 보병·창병과 비슷한 비율」.
/// </summary> /// </summary>
public float SizePx; public float SizePx;
/// <summary>말 탄 유닛(크기 표 mounted = 1) — 게임 배치 때 머리 끝 상한(BalanceConfig.mountedArtMaxTop)을 받는다.</summary>
public bool Mounted;
/// <summary>그림 속 몸(머리 끝~발) 세로 px — bodyHeightPx(무기 제외) 우선, 없으면 charHeightPx.</summary> /// <summary>그림 속 몸(머리 끝~발) 세로 px — bodyHeightPx(무기 제외) 우선, 없으면 charHeightPx.</summary>
public float BodyPx public float BodyPx
@ -68,36 +70,48 @@ namespace TKS
public const string SizeTable = "art_size"; public const string SizeTable = "art_size";
static readonly Dictionary<string, UnitArtData> cache = new Dictionary<string, UnitArtData>(); static readonly Dictionary<string, UnitArtData> cache = new Dictionary<string, UnitArtData>();
static Dictionary<string, float> sizes; static Dictionary<string, float> sizes;
static HashSet<string> mounted;
/// <summary>크기 표의 size_px(없으면 0).</summary> /// <summary>크기 표의 size_px(없으면 0).</summary>
public static float SizeOf(string id) public static float SizeOf(string id)
{ {
if (sizes == null) sizes = ParseSizes(Resources.Load<TextAsset>(Root + SizeTable)); EnsureSizes();
float v; float v;
return id != null && sizes.TryGetValue(id, out v) ? v : 0f; return id != null && sizes.TryGetValue(id, out v) ? v : 0f;
} }
static Dictionary<string, float> ParseSizes(TextAsset ta) /// <summary>크기 표 mounted = 1(말 탄 유닛).</summary>
public static bool IsMounted(string id)
{ {
var d = new Dictionary<string, float>(); EnsureSizes();
if (ta == null) return d; return id != null && mounted.Contains(id);
var lines = ta.text.TrimStart('').Split('\n');
if (lines.Length == 0) return d;
var head = new List<string>(lines[0].Trim().Split(','));
int ia = head.IndexOf("art"), isz = head.IndexOf("size_px");
if (ia < 0 || isz < 0)
{
Debug.LogWarning("[TKS] 유닛 크기 표 열(art · size_px) 없음");
return d;
} }
static void EnsureSizes()
{
if (sizes != null) return;
sizes = new Dictionary<string, float>();
mounted = new HashSet<string>();
var ta = Resources.Load<TextAsset>(Root + SizeTable);
if (ta == null) return;
var lines = ta.text.TrimStart('').Split('\n');
var head = new List<string>(lines[0].Trim().Split(','));
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 · mounted · size_px) 없음");
return;
}
int need = Mathf.Max(ia, Mathf.Max(isz, im));
for (int i = 1; i < lines.Length; i++) for (int i = 1; i < lines.Length; i++)
{ {
var c = lines[i].Trim().Split(','); var c = lines[i].Trim().Split(',');
if (c.Length <= need) continue;
float v; 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) if (float.TryParse(c[isz], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v) && v > 0f)
d[c[ia]] = v; sizes[c[ia]] = v;
if (c[im] == "1") mounted.Add(c[ia]);
} }
return d;
} }
public static UnitArtData Load(string id) public static UnitArtData Load(string id)
@ -114,6 +128,7 @@ namespace TKS
{ {
cache.Clear(); cache.Clear();
sizes = null; sizes = null;
mounted = null;
} }
static UnitArtData LoadUncached(string id) static UnitArtData LoadUncached(string id)
@ -132,7 +147,7 @@ namespace TKS
} }
if (meta == null || meta.clips == null) return null; 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) foreach (var c in meta.clips)
{ {
if (c == null || string.IsNullOrEmpty(c.name)) continue; if (c == null || string.IsNullOrEmpty(c.name)) continue;
@ -183,7 +198,8 @@ namespace TKS
public float TopHeight { get; private set; } public float TopHeight { get; private set; }
/// <param name="worldHeight">표준 키(월드) — 크기 표 size_px 가 이 높이가 되도록 배율(= 머리 크기 통일).</param> /// <param name="worldHeight">표준 키(월드) — 크기 표 size_px 가 이 높이가 되도록 배율(= 머리 크기 통일).</param>
public void Setup(UnitArtData d, float worldHeight, int sortingOrder) /// <param name="mountedMaxTop">말 탄 유닛의 머리 끝 상한 = worldHeight × 이 값(0 = 상한 없음 · 넘으면 배율을 줄임 = 탄 사람은 보병보다 조금 작아짐).</param>
public void Setup(UnitArtData d, float worldHeight, int sortingOrder, float mountedMaxTop = 0f)
{ {
data = d; data = d;
sr = GetComponent<SpriteRenderer>(); sr = GetComponent<SpriteRenderer>();
@ -191,8 +207,14 @@ namespace TKS
sr.sortingOrder = sortingOrder; sr.sortingOrder = sortingOrder;
float h = d.CharHeightWorld; float h = d.CharHeightWorld;
float s = h > 0.001f ? worldHeight / h : 1f; 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); 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) public bool Has(string clip)

View File

@ -139,7 +139,7 @@ namespace TKS
ago.transform.SetParent(transform, false); ago.transform.SetParent(transform, false);
ago.transform.localPosition = new Vector3(0f, -u.Radius * 0.55f, 0f); ago.transform.localPosition = new Vector3(0f, -u.Radius * 0.55f, 0f);
anim = ago.AddComponent<FrameAnimator>(); anim = ago.AddComponent<FrameAnimator>();
anim.Setup(art, u.ArtHeight, 0); anim.Setup(art, u.ArtHeight, 0, u.MountedArtMaxTop);
u.ArtHeight = anim.TopHeight; // 크기 표 배율 뒤 실제 머리 끝(HP 바 · 머리 위 표시 기준) u.ArtHeight = anim.TopHeight; // 크기 표 배율 뒤 실제 머리 끝(HP 바 · 머리 위 표시 기준)
anim.Renderer.flipX = facingLeft; anim.Renderer.flipX = facingLeft;
anim.SetIdle(ClipName("idle")); anim.SetIdle(ClipName("idle"));

View File

@ -1015,7 +1015,13 @@ namespace TKS.Tests
var u = bm.SpawnUnit(UnitType.Cavalry, Faction.Player, new Vector2(0f, -4f), "조운", 1f, 1f, false, "zhaoyun"); var u = bm.SpawnUnit(UnitType.Cavalry, Faction.Player, new Vector2(0f, -4f), "조운", 1f, 1f, false, "zhaoyun");
yield return null; yield return null;
Assert.AreEqual(u.View.Anim.TopHeight, u.ArtHeight, 1e-4f, "ArtHeight = 그려진 머리 끝"); 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) ---------- // ---------- 스테이지 표(Assets/Data/Stages/*.csv · 145 스테이지 · 2026-11-05 #1086) ----------