4방향 보기 #1082 — 마지막 이동 방향으로 앞/뒤 클립 세트 선택 + 좌우 반전(↘ 기본 · ↙ · ↖ · ↗) · 슬링샷·튕김·밀림·돌진·사격·밀린 아군 공격 시 갱신 · 기병 복귀는 방향 유지 · 테스트 23/23
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
852261b51d
commit
8f71910b6b
|
|
@ -210,6 +210,7 @@ namespace TKS
|
||||||
float travel = Combat.FreeTravel(first, first.Position, dir, remaining, true, true, u);
|
float travel = Combat.FreeTravel(first, first.Position, dir, remaining, true, true, u);
|
||||||
if (travel > 0.01f)
|
if (travel > 0.01f)
|
||||||
{
|
{
|
||||||
|
Face(first, dir); // 밀린 유닛은 밀린 방향을 본다(PD #1082)
|
||||||
Vector2 firstEnd = first.Position + dir * travel;
|
Vector2 firstEnd = first.Position + dir * travel;
|
||||||
Vector2 spearEnd = contact + dir * travel;
|
Vector2 spearEnd = contact + dir * travel;
|
||||||
Fx.Run(Fx.MoveAlong(first.transform, first.Position, firstEnd, cfg.moveSpeed, null));
|
Fx.Run(Fx.MoveAlong(first.transform, first.Position, firstEnd, cfg.moveSpeed, null));
|
||||||
|
|
@ -251,6 +252,7 @@ namespace TKS
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Face(u, dir);
|
||||||
if (u.View != null) u.View.PlayAttack();
|
if (u.View != null) u.View.PlayAttack();
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
var hit = new List<Unit>();
|
var hit = new List<Unit>();
|
||||||
|
|
@ -291,7 +293,7 @@ namespace TKS
|
||||||
if (willReturn && u.IsAlive)
|
if (willReturn && u.IsAlive)
|
||||||
{
|
{
|
||||||
yield return Fx.Wait(0.1f);
|
yield return Fx.Wait(0.1f);
|
||||||
yield return Move(u, origin, o);
|
yield return Move(u, origin, o, false); // 복귀는 적을 본 채 물러남(보는 방향 유지 · 총괄PM 해석)
|
||||||
o.Returned = true;
|
o.Returned = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -386,6 +388,7 @@ namespace TKS
|
||||||
if (hitDist - freeMove <= u.AttackRange + 0.05f)
|
if (hitDist - freeMove <= u.AttackRange + 0.05f)
|
||||||
{
|
{
|
||||||
bool crit = Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == first;
|
bool crit = Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == first;
|
||||||
|
Face(u, dir); // 제자리 사격도 조준 방향을 본다
|
||||||
yield return Attack(u, first, true, crit, o, true);
|
yield return Attack(u, first, true, crit, o, true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -403,8 +406,15 @@ namespace TKS
|
||||||
o.MovedOnly = true;
|
o.MovedOnly = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerator Move(Unit u, Vector2 to, ActionOutcome o)
|
/// <summary>보는 방향 갱신(PD #1082 · 마지막 이동 방향 · 4방향 = 앞/뒤 세트 + 좌우 반전).</summary>
|
||||||
|
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);
|
yield return Fx.MoveAlong(u.transform, u.Position, to, cfg.moveSpeed, null);
|
||||||
u.Position = to;
|
u.Position = to;
|
||||||
o.Path.Add(to);
|
o.Path.Add(to);
|
||||||
|
|
@ -413,6 +423,7 @@ namespace TKS
|
||||||
/// <summary>등속 접근(감속 없음) — 부딪히는 순간까지 속도를 유지해 충돌 반응이 바로 이어진다.</summary>
|
/// <summary>등속 접근(감속 없음) — 부딪히는 순간까지 속도를 유지해 충돌 반응이 바로 이어진다.</summary>
|
||||||
IEnumerator MoveStraight(Unit u, Vector2 to, ActionOutcome o)
|
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);
|
yield return Fx.MoveAlong(u.transform, u.Position, to, cfg.moveSpeed, null, false);
|
||||||
u.Position = to;
|
u.Position = to;
|
||||||
o.Path.Add(to);
|
o.Path.Add(to);
|
||||||
|
|
@ -453,6 +464,7 @@ namespace TKS
|
||||||
float back = Combat.FreeTravel(striker, striker.Position, -n, self, true, true, struck);
|
float back = Combat.FreeTravel(striker, striker.Position, -n, self, true, true, struck);
|
||||||
if (back > 0.01f)
|
if (back > 0.01f)
|
||||||
{
|
{
|
||||||
|
Face(striker, -n); // 튕겨 나온 방향을 본다(PD #1082)
|
||||||
Vector2 end = battle.Board.ClampInside(striker.Position - n * back, striker.Radius);
|
Vector2 end = battle.Board.ClampInside(striker.Position - n * back, striker.Radius);
|
||||||
yield return Fx.MoveAlong(striker.transform, striker.Position, end, cfg.moveSpeed, null);
|
yield return Fx.MoveAlong(striker.transform, striker.Position, end, cfg.moveSpeed, null);
|
||||||
striker.Position = end;
|
striker.Position = end;
|
||||||
|
|
@ -475,6 +487,7 @@ namespace TKS
|
||||||
float slide = Combat.FreeTravel(target, target.Position, n, force, true, true, pusher);
|
float slide = Combat.FreeTravel(target, target.Position, n, force, true, true, pusher);
|
||||||
if (slide > 0.01f)
|
if (slide > 0.01f)
|
||||||
{
|
{
|
||||||
|
Face(target, n); // 밀린 방향을 본다(PD #1082)
|
||||||
Vector2 end = battle.Board.ClampInside(target.Position + n * slide, target.Radius);
|
Vector2 end = battle.Board.ClampInside(target.Position + n * slide, target.Radius);
|
||||||
yield return Fx.MoveAlong(target.transform, target.Position, end, cfg.moveSpeed, null);
|
yield return Fx.MoveAlong(target.transform, target.Position, end, cfg.moveSpeed, null);
|
||||||
target.Position = end;
|
target.Position = end;
|
||||||
|
|
@ -490,6 +503,7 @@ namespace TKS
|
||||||
Unit foe = Combat.TouchingOpponent(ally);
|
Unit foe = Combat.TouchingOpponent(ally);
|
||||||
if (foe == null) yield break;
|
if (foe == null) yield break;
|
||||||
yield return Fx.Wait(0.1f);
|
yield return Fx.Wait(0.1f);
|
||||||
|
Face(ally, foe.Position - ally.Position);
|
||||||
yield return Attack(ally, foe, false, false, o, false);
|
yield return Attack(ally, foe, false, false, o, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ namespace TKS
|
||||||
Color baseColor;
|
Color baseColor;
|
||||||
Coroutine flash;
|
Coroutine flash;
|
||||||
bool aiming;
|
bool aiming;
|
||||||
|
bool facingBack; // 위를 봄(뒷모습 back_* 세트) · false = 아래를 봄(앞모습 front_*)
|
||||||
|
bool facingLeft; // 왼쪽을 봄(좌우 반전) · false = 오른쪽(기본)
|
||||||
|
const float FacingEps = 0.05f;
|
||||||
|
|
||||||
/// <summary>스프라이트 아트로 그리는가(false = 도형).</summary>
|
/// <summary>스프라이트 아트로 그리는가(false = 도형).</summary>
|
||||||
public bool HasArt
|
public bool HasArt
|
||||||
|
|
@ -45,6 +48,52 @@ namespace TKS
|
||||||
get { return anim; }
|
get { return anim; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>4방향 보기(PD #1082): 위를 보면 뒷모습(↖·↗) · 아래를 보면 앞모습(↙·↘).</summary>
|
||||||
|
public bool FacingBack
|
||||||
|
{
|
||||||
|
get { return facingBack; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>왼쪽을 보면 좌우 반전(↙·↖) · 오른쪽이 기본(↘·↗).</summary>
|
||||||
|
public bool FacingLeft
|
||||||
|
{
|
||||||
|
get { return facingLeft; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 마지막 이동 방향으로 보는 방향을 정한다(PD #1082 · 슬링샷·튕김·밀림·돌진 뒤 마지막 방향).
|
||||||
|
/// 세로 성분이 있으면 앞/뒤 세트를, 가로 성분이 있으면 좌우 반전을 바꾼다(거의 0 인 축은 이전 값 유지).
|
||||||
|
/// </summary>
|
||||||
|
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)
|
public void Build(Unit u)
|
||||||
{
|
{
|
||||||
unit = u;
|
unit = u;
|
||||||
|
|
@ -65,6 +114,10 @@ namespace TKS
|
||||||
|
|
||||||
body = ProceduralSprites.NewSprite(transform, "Body", ProceduralSprites.Circle, baseColor, d, 0);
|
body = ProceduralSprites.NewSprite(transform, "Body", ProceduralSprites.Circle, baseColor, d, 0);
|
||||||
|
|
||||||
|
// 시작 보기: 아군 = 위(↗ 뒷모습) · 적군 = 아래(↘ 앞모습) · 이후는 마지막 이동 방향(PD #1082)
|
||||||
|
facingBack = u.Faction == Faction.Player;
|
||||||
|
facingLeft = false;
|
||||||
|
|
||||||
// 아트가 있으면 몸통 원 대신 스프라이트 애니(발 = 원 아래쪽)
|
// 아트가 있으면 몸통 원 대신 스프라이트 애니(발 = 원 아래쪽)
|
||||||
anim = null;
|
anim = null;
|
||||||
var art = UnitArt.Load(u.ArtId);
|
var art = UnitArt.Load(u.ArtId);
|
||||||
|
|
@ -76,6 +129,7 @@ namespace TKS
|
||||||
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);
|
||||||
|
anim.Renderer.flipX = facingLeft;
|
||||||
anim.SetIdle(ClipName("idle"));
|
anim.SetIdle(ClipName("idle"));
|
||||||
anim.PlayIdle();
|
anim.PlayIdle();
|
||||||
}
|
}
|
||||||
|
|
@ -105,11 +159,11 @@ namespace TKS
|
||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>보기(아군 = back · 적군 = front)에 맞는 클립 이름. 그 보기에 없으면 다른 보기 → 대기.</summary>
|
/// <summary>보는 방향(위 = back · 아래 = front)에 맞는 클립 이름. 그 보기에 없으면 다른 보기 → 대기.</summary>
|
||||||
string ClipName(string suffix)
|
string ClipName(string suffix)
|
||||||
{
|
{
|
||||||
string v = unit.Faction == Faction.Player ? "back" : "front";
|
string v = facingBack ? "back" : "front";
|
||||||
string o = unit.Faction == Faction.Player ? "front" : "back";
|
string o = facingBack ? "front" : "back";
|
||||||
if (anim.Has(v + "_" + suffix)) return v + "_" + suffix;
|
if (anim.Has(v + "_" + suffix)) return v + "_" + suffix;
|
||||||
if (anim.Has(o + "_" + suffix)) return o + "_" + suffix;
|
if (anim.Has(o + "_" + suffix)) return o + "_" + suffix;
|
||||||
return anim.Has(v + "_idle") ? v + "_idle" : o + "_idle";
|
return anim.Has(v + "_idle") ? v + "_idle" : o + "_idle";
|
||||||
|
|
|
||||||
|
|
@ -501,5 +501,45 @@ namespace TKS.Tests
|
||||||
bm.StartStage(0);
|
bm.StartStage(0);
|
||||||
yield return null;
|
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, "밀린 적은 밀린 방향(위)을 본다");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue