기마 크기 상한 ×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:
parent
c64be33646
commit
b27edb3c49
|
|
@ -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 |
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 ⑤) = 궁병과 같은 사거리 규칙
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 lines = ta.text.TrimStart('').Split('\n');
|
||||||
if (lines.Length == 0) return d;
|
|
||||||
var head = new List<string>(lines[0].Trim().Split(','));
|
var head = new List<string>(lines[0].Trim().Split(','));
|
||||||
int ia = head.IndexOf("art"), isz = head.IndexOf("size_px");
|
int ia = head.IndexOf("art"), isz = head.IndexOf("size_px"), im = head.IndexOf("mounted");
|
||||||
if (ia < 0 || isz < 0)
|
if (ia < 0 || isz < 0 || im < 0)
|
||||||
{
|
{
|
||||||
Debug.LogWarning("[TKS] 유닛 크기 표 열(art · size_px) 없음");
|
Debug.LogWarning("[TKS] 유닛 크기 표 열(art · mounted · size_px) 없음");
|
||||||
return d;
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
|
|
|
||||||
|
|
@ -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) ----------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue