From 8f71910b6b0fd2266dab992ef1f44e248915de5d Mon Sep 17 00:00:00 2001 From: swrring Date: Wed, 4 Nov 2026 17:58:30 +0900 Subject: [PATCH] =?UTF-8?q?4=EB=B0=A9=ED=96=A5=20=EB=B3=B4=EA=B8=B0=20#108?= =?UTF-8?q?2=20=E2=80=94=20=EB=A7=88=EC=A7=80=EB=A7=89=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EB=B0=A9=ED=96=A5=EC=9C=BC=EB=A1=9C=20=EC=95=9E/?= =?UTF-8?q?=EB=92=A4=20=ED=81=B4=EB=A6=BD=20=EC=84=B8=ED=8A=B8=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20+=20=EC=A2=8C=EC=9A=B0=20=EB=B0=98=EC=A0=84(?= =?UTF-8?q?=E2=86=98=20=EA=B8=B0=EB=B3=B8=20=C2=B7=20=E2=86=99=20=C2=B7=20?= =?UTF-8?q?=E2=86=96=20=C2=B7=20=E2=86=97)=20=C2=B7=20=EC=8A=AC=EB=A7=81?= =?UTF-8?q?=EC=83=B7=C2=B7=ED=8A=95=EA=B9=80=C2=B7=EB=B0=80=EB=A6=BC=C2=B7?= =?UTF-8?q?=EB=8F=8C=EC=A7=84=C2=B7=EC=82=AC=EA=B2=A9=C2=B7=EB=B0=80?= =?UTF-8?q?=EB=A6=B0=20=EC=95=84=EA=B5=B0=20=EA=B3=B5=EA=B2=A9=20=EC=8B=9C?= =?UTF-8?q?=20=EA=B0=B1=EC=8B=A0=20=C2=B7=20=EA=B8=B0=EB=B3=91=20=EB=B3=B5?= =?UTF-8?q?=EA=B7=80=EB=8A=94=20=EB=B0=A9=ED=96=A5=20=EC=9C=A0=EC=A7=80=20?= =?UTF-8?q?=C2=B7=20=ED=85=8C=EC=8A=A4=ED=8A=B8=2023/23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UnitView: FacingBack/FacingLeft · SetFacing(세로 성분 → back/front 세트 · 가로 성분 → flipX · 거의 0 인 축은 유지 · 세트가 바뀌면 같은 동작을 새 보기 클립으로 이어 재생) · 시작 = 아군 ↗ · 적군 ↘ - ActionResolver: Face() · Move/MoveStraight/Push/Bounce/플로우/기병 돌진/궁병 사격/밀린 아군 공격에서 호출 · 기병 복귀 Move(face:false) - 테스트 Facing_FourWays_FollowsLastMoveDirection_BounceTurnsAround(유비 아트 4방향 + 튕김 뒤 아래 보기 + 밀린 적 위 보기) Co-Authored-By: Claude Fable 5.1 --- Assets/Scripts/Runtime/ActionResolver.cs | 18 ++++++- Assets/Scripts/Runtime/UnitView.cs | 60 ++++++++++++++++++++++-- Assets/Tests/PlayMode/BattleFlowTests.cs | 40 ++++++++++++++++ 3 files changed, 113 insertions(+), 5 deletions(-) diff --git a/Assets/Scripts/Runtime/ActionResolver.cs b/Assets/Scripts/Runtime/ActionResolver.cs index 68e5d170..d7aa20cc 100644 --- a/Assets/Scripts/Runtime/ActionResolver.cs +++ b/Assets/Scripts/Runtime/ActionResolver.cs @@ -210,6 +210,7 @@ namespace TKS float travel = Combat.FreeTravel(first, first.Position, dir, remaining, true, true, u); if (travel > 0.01f) { + Face(first, dir); // 밀린 유닛은 밀린 방향을 본다(PD #1082) Vector2 firstEnd = first.Position + dir * travel; Vector2 spearEnd = contact + dir * travel; Fx.Run(Fx.MoveAlong(first.transform, first.Position, firstEnd, cfg.moveSpeed, null)); @@ -251,6 +252,7 @@ namespace TKS yield break; } + Face(u, dir); if (u.View != null) u.View.PlayAttack(); int idx = 0; var hit = new List(); @@ -291,7 +293,7 @@ namespace TKS if (willReturn && u.IsAlive) { yield return Fx.Wait(0.1f); - yield return Move(u, origin, o); + yield return Move(u, origin, o, false); // 복귀는 적을 본 채 물러남(보는 방향 유지 · 총괄PM 해석) o.Returned = true; } } @@ -386,6 +388,7 @@ namespace TKS if (hitDist - freeMove <= u.AttackRange + 0.05f) { bool crit = Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == first; + Face(u, dir); // 제자리 사격도 조준 방향을 본다 yield return Attack(u, first, true, crit, o, true); } else @@ -403,8 +406,15 @@ namespace TKS o.MovedOnly = true; } - IEnumerator Move(Unit u, Vector2 to, ActionOutcome o) + /// 보는 방향 갱신(PD #1082 · 마지막 이동 방향 · 4방향 = 앞/뒤 세트 + 좌우 반전). + static void Face(Unit u, Vector2 dir) { + if (u != null && u.View != null && dir.sqrMagnitude > 1e-6f) u.View.SetFacing(dir); + } + + IEnumerator Move(Unit u, Vector2 to, ActionOutcome o, bool face = true) + { + if (face) Face(u, to - u.Position); yield return Fx.MoveAlong(u.transform, u.Position, to, cfg.moveSpeed, null); u.Position = to; o.Path.Add(to); @@ -413,6 +423,7 @@ namespace TKS /// 등속 접근(감속 없음) — 부딪히는 순간까지 속도를 유지해 충돌 반응이 바로 이어진다. IEnumerator MoveStraight(Unit u, Vector2 to, ActionOutcome o) { + Face(u, to - u.Position); yield return Fx.MoveAlong(u.transform, u.Position, to, cfg.moveSpeed, null, false); u.Position = to; o.Path.Add(to); @@ -453,6 +464,7 @@ namespace TKS float back = Combat.FreeTravel(striker, striker.Position, -n, self, true, true, struck); if (back > 0.01f) { + Face(striker, -n); // 튕겨 나온 방향을 본다(PD #1082) Vector2 end = battle.Board.ClampInside(striker.Position - n * back, striker.Radius); yield return Fx.MoveAlong(striker.transform, striker.Position, end, cfg.moveSpeed, null); striker.Position = end; @@ -475,6 +487,7 @@ namespace TKS float slide = Combat.FreeTravel(target, target.Position, n, force, true, true, pusher); if (slide > 0.01f) { + Face(target, n); // 밀린 방향을 본다(PD #1082) Vector2 end = battle.Board.ClampInside(target.Position + n * slide, target.Radius); yield return Fx.MoveAlong(target.transform, target.Position, end, cfg.moveSpeed, null); target.Position = end; @@ -490,6 +503,7 @@ namespace TKS Unit foe = Combat.TouchingOpponent(ally); if (foe == null) yield break; yield return Fx.Wait(0.1f); + Face(ally, foe.Position - ally.Position); yield return Attack(ally, foe, false, false, o, false); } diff --git a/Assets/Scripts/Runtime/UnitView.cs b/Assets/Scripts/Runtime/UnitView.cs index bace417a..362cbd9c 100644 --- a/Assets/Scripts/Runtime/UnitView.cs +++ b/Assets/Scripts/Runtime/UnitView.cs @@ -33,6 +33,9 @@ namespace TKS Color baseColor; Coroutine flash; bool aiming; + bool facingBack; // 위를 봄(뒷모습 back_* 세트) · false = 아래를 봄(앞모습 front_*) + bool facingLeft; // 왼쪽을 봄(좌우 반전) · false = 오른쪽(기본) + const float FacingEps = 0.05f; /// 스프라이트 아트로 그리는가(false = 도형). public bool HasArt @@ -45,6 +48,52 @@ namespace TKS get { return anim; } } + /// 4방향 보기(PD #1082): 위를 보면 뒷모습(↖·↗) · 아래를 보면 앞모습(↙·↘). + public bool FacingBack + { + get { return facingBack; } + } + + /// 왼쪽을 보면 좌우 반전(↙·↖) · 오른쪽이 기본(↘·↗). + public bool FacingLeft + { + get { return facingLeft; } + } + + /// + /// 마지막 이동 방향으로 보는 방향을 정한다(PD #1082 · 슬링샷·튕김·밀림·돌진 뒤 마지막 방향). + /// 세로 성분이 있으면 앞/뒤 세트를, 가로 성분이 있으면 좌우 반전을 바꾼다(거의 0 인 축은 이전 값 유지). + /// + public void SetFacing(Vector2 dir) + { + bool viewChanged = false; + if (dir.y > FacingEps && !facingBack) + { + facingBack = true; + viewChanged = true; + } + else if (dir.y < -FacingEps && facingBack) + { + facingBack = false; + viewChanged = true; + } + if (dir.x > FacingEps) facingLeft = false; + else if (dir.x < -FacingEps) facingLeft = true; + + if (anim == null) return; + anim.Renderer.flipX = facingLeft; + if (!viewChanged) return; + + // 앞/뒤 세트가 바뀌면 지금 동작을 새 보기의 같은 클립으로 이어 간다 + string cur = anim.Current ?? ""; + string suffix = cur.StartsWith("back_") ? cur.Substring(5) : (cur.StartsWith("front_") ? cur.Substring(6) : "idle"); + anim.SetIdle(ClipName("idle")); + if (suffix == "idle") anim.PlayIdle(); + else if (suffix == "ready") anim.Play(ClipName("ready"), true); + else if (suffix == "victory") anim.Play(ClipName("victory"), false); + else anim.Play(ClipName(suffix), false, AttackHold, true); + } + public void Build(Unit u) { unit = u; @@ -65,6 +114,10 @@ namespace TKS body = ProceduralSprites.NewSprite(transform, "Body", ProceduralSprites.Circle, baseColor, d, 0); + // 시작 보기: 아군 = 위(↗ 뒷모습) · 적군 = 아래(↘ 앞모습) · 이후는 마지막 이동 방향(PD #1082) + facingBack = u.Faction == Faction.Player; + facingLeft = false; + // 아트가 있으면 몸통 원 대신 스프라이트 애니(발 = 원 아래쪽) anim = null; var art = UnitArt.Load(u.ArtId); @@ -76,6 +129,7 @@ namespace TKS ago.transform.localPosition = new Vector3(0f, -u.Radius * 0.55f, 0f); anim = ago.AddComponent(); anim.Setup(art, u.ArtHeight, 0); + anim.Renderer.flipX = facingLeft; anim.SetIdle(ClipName("idle")); anim.PlayIdle(); } @@ -105,11 +159,11 @@ namespace TKS Refresh(); } - /// 보기(아군 = back · 적군 = front)에 맞는 클립 이름. 그 보기에 없으면 다른 보기 → 대기. + /// 보는 방향(위 = back · 아래 = front)에 맞는 클립 이름. 그 보기에 없으면 다른 보기 → 대기. string ClipName(string suffix) { - string v = unit.Faction == Faction.Player ? "back" : "front"; - string o = unit.Faction == Faction.Player ? "front" : "back"; + string v = facingBack ? "back" : "front"; + string o = facingBack ? "front" : "back"; if (anim.Has(v + "_" + suffix)) return v + "_" + suffix; if (anim.Has(o + "_" + suffix)) return o + "_" + suffix; return anim.Has(v + "_idle") ? v + "_idle" : o + "_idle"; diff --git a/Assets/Tests/PlayMode/BattleFlowTests.cs b/Assets/Tests/PlayMode/BattleFlowTests.cs index 41d391f6..e60957d1 100644 --- a/Assets/Tests/PlayMode/BattleFlowTests.cs +++ b/Assets/Tests/PlayMode/BattleFlowTests.cs @@ -501,5 +501,45 @@ namespace TKS.Tests bm.StartStage(0); yield return null; } + + [UnityTest] + public IEnumerator Facing_FourWays_FollowsLastMoveDirection_BounceTurnsAround() + { + yield return Fresh(); + // PD #1082: 오른쪽 보기(기본) ↘ · 왼쪽 ↙ · 왼쪽 위 ↖ · 오른쪽 위 ↗ = 앞/뒤 세트 + 좌우 반전 · 보는 방향 = 마지막 이동 방향 + var u = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -5f), "유비", 1f, 1f, false, "liubei"); + Assert.IsTrue(u.View.HasArt); + Assert.IsTrue(u.View.FacingBack && !u.View.FacingLeft, "아군 시작 = ↗(위 · 오른쪽)"); + Assert.AreEqual("back_idle", u.View.Anim.Current); + + yield return bm.ResolveForTest(u, u.Position + new Vector2(2f, 2f), false, new ActionOutcome()); + Assert.IsTrue(u.View.FacingBack && !u.View.FacingLeft, "↗ 이동 → 뒷모습 · 반전 없음"); + Assert.IsFalse(u.View.Anim.Renderer.flipX); + + yield return bm.ResolveForTest(u, u.Position + new Vector2(-2f, 2f), false, new ActionOutcome()); + Assert.IsTrue(u.View.FacingBack && u.View.FacingLeft, "↖ 이동 → 뒷모습 · 좌우 반전"); + Assert.IsTrue(u.View.Anim.Renderer.flipX); + Assert.AreEqual("back_idle", u.View.Anim.Current); + + yield return bm.ResolveForTest(u, u.Position + new Vector2(-2f, -2f), false, new ActionOutcome()); + Assert.IsTrue(!u.View.FacingBack && u.View.FacingLeft, "↙ 이동 → 앞모습 · 좌우 반전"); + Assert.AreEqual("front_idle", u.View.Anim.Current, "앞/뒤 세트가 바뀌면 대기 클립도 바뀐다"); + + yield return bm.ResolveForTest(u, u.Position + new Vector2(2f, -2f), false, new ActionOutcome()); + Assert.IsTrue(!u.View.FacingBack && !u.View.FacingLeft, "↘ 이동 → 앞모습 · 반전 없음"); + Assert.IsFalse(u.View.Anim.Renderer.flipX); + + yield return bm.ResolveForTest(u, u.Position + new Vector2(0f, 2f), false, new ActionOutcome()); + Assert.IsTrue(u.View.FacingBack && !u.View.FacingLeft, "곧장 위로 → 뒷모습 · 좌우는 이전 값 유지"); + + // 위의 적을 쳐서 튕겨 나오면(아래로) 아래를 보고, 밀린 적은 밀린 방향(위)을 본다 + var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, u.Position + new Vector2(0f, 2.5f), "적보병"); + Assert.IsFalse(foe.View.FacingBack, "적 시작 = ↘(아래)"); + var o = new ActionOutcome(); + yield return bm.ResolveForTest(u, foe.Position, false, o); + Assert.Contains(foe, o.Pushed); + Assert.IsFalse(u.View.FacingBack, "튕겨 나와 방향이 바뀌면 그 방향(아래)을 본다"); + Assert.IsTrue(foe.View.FacingBack, "밀린 적은 밀린 방향(위)을 본다"); + } } }