255 lines
12 KiB
C#
255 lines
12 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.TestTools;
|
|
|
|
namespace TKS.Tests
|
|
{
|
|
/// <summary>병종 4종 규칙·턴 흐름·승리 판정을 실제 씬에서 검증한다(헤드리스 실행 가능).</summary>
|
|
public class BattleFlowTests
|
|
{
|
|
BattleManager bm;
|
|
BalanceConfig cfg;
|
|
|
|
[UnitySetUp]
|
|
public IEnumerator SetUp()
|
|
{
|
|
SceneManager.LoadScene("Battle");
|
|
yield return null;
|
|
yield return null;
|
|
bm = BattleManager.Instance;
|
|
Assert.IsNotNull(bm, "GameBootstrap 이 BattleManager 를 만들어야 한다");
|
|
cfg = bm.Config;
|
|
Time.timeScale = 20f;
|
|
}
|
|
|
|
[UnityTearDown]
|
|
public IEnumerator TearDown()
|
|
{
|
|
Time.timeScale = 1f;
|
|
yield return null;
|
|
}
|
|
|
|
IEnumerator Fresh()
|
|
{
|
|
bm.StopAllCoroutines();
|
|
bm.ClearUnits();
|
|
bm.ForcePhase(BattlePhase.PlayerTurn);
|
|
yield return null; // 파괴된 콜라이더가 물리 월드에서 빠질 시간
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Bootstrap_SpawnsHeroesAndStageEnemies()
|
|
{
|
|
Assert.AreEqual(BattlePhase.PlayerTurn, bm.Phase);
|
|
Assert.AreEqual(Mathf.Min(5, cfg.heroes.Length), bm.AliveCount(Faction.Player));
|
|
Assert.AreEqual(cfg.stages[0].enemies.Count, bm.AliveCount(Faction.Enemy));
|
|
yield return null;
|
|
}
|
|
|
|
[Test]
|
|
public void Matchup_TriangleAndArcherWeakness()
|
|
{
|
|
var c = cfg != null ? cfg : Resources.Load<BalanceConfig>("BalanceConfig");
|
|
Assert.Greater(c.Matchup(UnitType.Infantry, UnitType.Spear), 1f);
|
|
Assert.Greater(c.Matchup(UnitType.Spear, UnitType.Cavalry), 1f);
|
|
Assert.Greater(c.Matchup(UnitType.Cavalry, UnitType.Infantry), 1f);
|
|
Assert.Less(c.Matchup(UnitType.Spear, UnitType.Infantry), 1f);
|
|
Assert.Less(c.Matchup(UnitType.Cavalry, UnitType.Spear), 1f);
|
|
Assert.Less(c.Matchup(UnitType.Infantry, UnitType.Cavalry), 1f);
|
|
Assert.Greater(c.Matchup(UnitType.Infantry, UnitType.Archer), 1f);
|
|
Assert.Greater(c.Matchup(UnitType.Cavalry, UnitType.Archer), 1f);
|
|
Assert.Greater(c.Matchup(UnitType.Spear, UnitType.Archer), 1f);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Infantry_AttacksFirstEnemyOnPath_BonusMoveOnFinalHit()
|
|
{
|
|
yield return Fresh();
|
|
var inf = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -3f), "보병");
|
|
var foe = bm.SpawnUnit(UnitType.Spear, Faction.Enemy, new Vector2(0f, 0f), "적창병");
|
|
var o = new ActionOutcome();
|
|
yield return bm.ResolveForTest(inf, foe.Position, false, o);
|
|
|
|
Assert.Contains(foe, o.Attacked);
|
|
Assert.Less(foe.Hp, foe.MaxHp, "경로 위 첫 적을 공격");
|
|
Assert.IsTrue(o.BonusMove, "최종 목표점 명중 → 추가 이동");
|
|
Assert.Less(Vector2.Distance(inf.Position, foe.Position), inf.Radius + foe.Radius + 0.2f, "적에 닿는 지점까지 이동");
|
|
Assert.Less(inf.Hp, inf.MaxHp, "근접 공격은 반격 피해를 받는다");
|
|
|
|
int foeHp = foe.Hp;
|
|
var o2 = new ActionOutcome();
|
|
yield return bm.ResolveForTest(inf, inf.Position + new Vector2(2.5f, 0f), true, o2);
|
|
Assert.AreEqual(0, o2.Attacked.Count, "추가 이동은 이동만");
|
|
Assert.AreEqual(foeHp, foe.Hp);
|
|
Assert.IsTrue(o2.MovedOnly);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Infantry_NoBonus_WhenTargetBeyondEnemy()
|
|
{
|
|
yield return Fresh();
|
|
var inf = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -3f), "보병");
|
|
var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 0f), "적보병");
|
|
var o = new ActionOutcome();
|
|
yield return bm.ResolveForTest(inf, new Vector2(0f, 1.4f), false, o);
|
|
Assert.Contains(foe, o.Attacked);
|
|
Assert.IsFalse(o.BonusMove);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Cavalry_PiercesAllOnPath_AndReturnsOnFinalHit()
|
|
{
|
|
yield return Fresh();
|
|
var cav = bm.SpawnUnit(UnitType.Cavalry, Faction.Player, new Vector2(0f, -4f), "기병");
|
|
var e1 = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, -1.5f), "적1");
|
|
var e2 = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 1f), "적2");
|
|
var o = new ActionOutcome();
|
|
yield return bm.ResolveForTest(cav, e2.Position, false, o);
|
|
Assert.Less(e1.Hp, e1.MaxHp, "경로 위 첫 적 관통 피해");
|
|
Assert.Less(e2.Hp, e2.MaxHp, "경로 위 둘째 적 관통 피해");
|
|
Assert.IsTrue(o.Returned, "최종 목표점 명중 → 제자리 복귀");
|
|
Assert.Less(Vector2.Distance(cav.Position, new Vector2(0f, -4f)), 0.05f);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Cavalry_StaysAtEnd_WithoutFinalHit()
|
|
{
|
|
yield return Fresh();
|
|
var cav = bm.SpawnUnit(UnitType.Cavalry, Faction.Player, new Vector2(0f, -4f), "기병");
|
|
var e1 = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, -1f), "적1");
|
|
var o = new ActionOutcome();
|
|
yield return bm.ResolveForTest(cav, new Vector2(0f, 2f), false, o);
|
|
Assert.Less(e1.Hp, e1.MaxHp);
|
|
Assert.IsFalse(o.Returned);
|
|
Assert.Greater(cav.Position.y, 1.5f, "목표점까지 이동");
|
|
Assert.GreaterOrEqual(Vector2.Distance(cav.Position, e1.Position), cav.Radius + e1.Radius - 0.05f, "적과 겹치지 않음");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Archer_MovesIntoRange_ShootsWithoutCounter()
|
|
{
|
|
yield return Fresh();
|
|
var arc = bm.SpawnUnit(UnitType.Archer, Faction.Player, new Vector2(0f, -5f), "궁병");
|
|
var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 0f), "적보병");
|
|
var o = new ActionOutcome();
|
|
yield return bm.ResolveForTest(arc, new Vector2(0f, -1f), false, o);
|
|
Assert.Contains(foe, o.Attacked, "목표 방향의 적을 사거리까지 이동 후 사격");
|
|
Assert.Less(foe.Hp, foe.MaxHp);
|
|
Assert.AreEqual(arc.MaxHp, arc.Hp, "궁병은 반격을 받지 않는다");
|
|
Assert.LessOrEqual(Vector2.Distance(arc.Position, foe.Position), arc.AttackRange + 0.1f);
|
|
Assert.IsFalse(o.Crit);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Archer_CritOnFinalHit()
|
|
{
|
|
yield return Fresh();
|
|
var arc = bm.SpawnUnit(UnitType.Archer, Faction.Player, new Vector2(0f, -3f), "궁병");
|
|
var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 0f), "적보병");
|
|
var o = new ActionOutcome();
|
|
yield return bm.ResolveForTest(arc, foe.Position, false, o);
|
|
Assert.Contains(foe, o.Attacked);
|
|
Assert.IsTrue(o.Crit, "최종 목표점 명중 → 크리티컬");
|
|
Assert.AreEqual(arc.MaxHp, arc.Hp);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Spear_OverwatchOnFinalHit_DamagesMovingEnemy()
|
|
{
|
|
yield return Fresh();
|
|
var spear = bm.SpawnUnit(UnitType.Spear, Faction.Player, new Vector2(0f, -3f), "창병");
|
|
var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 0f), "적보병");
|
|
var o = new ActionOutcome();
|
|
yield return bm.ResolveForTest(spear, foe.Position, false, o);
|
|
Assert.IsTrue(o.Overwatch && spear.Overwatch, "최종 목표점 명중 → 대기(견제)");
|
|
|
|
int hp = foe.Hp;
|
|
var path = new List<Vector2> { foe.Position, foe.Position + new Vector2(1.5f, 0f) };
|
|
yield return bm.ApplyOverwatch(foe, path);
|
|
Assert.Less(foe.Hp, hp, "반경 안에서 움직인 적에게 피해");
|
|
|
|
// 반경 밖에서만 움직인 적은 피해 없음
|
|
var far = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(5f, 6f), "먼적");
|
|
int farHp = far.Hp;
|
|
yield return bm.ApplyOverwatch(far, new List<Vector2> { far.Position, far.Position + new Vector2(-1f, 0f) });
|
|
Assert.AreEqual(farHp, far.Hp);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator EnemyTurn_RunsAndReturnsToPlayerTurn()
|
|
{
|
|
yield return Fresh();
|
|
var p = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -3f), "아군");
|
|
var e = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 3f), "적군");
|
|
p.MarkActed();
|
|
int turn = bm.TurnNumber;
|
|
yield return bm.RunEnemyTurn();
|
|
Assert.AreEqual(BattlePhase.PlayerTurn, bm.Phase);
|
|
Assert.AreEqual(turn + 1, bm.TurnNumber);
|
|
Assert.IsFalse(p.ActedThisTurn, "새 턴에 행동 플래그 초기화");
|
|
Assert.IsTrue(e.Position.y < 2.9f || p.Hp < p.MaxHp, "적이 접근하거나 공격했다");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator PlayerActions_AutoEndTurn_ThenEnemyTurn()
|
|
{
|
|
yield return Fresh();
|
|
var p = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(-3f, -5f), "아군");
|
|
var e = bm.SpawnUnit(UnitType.Archer, Faction.Enemy, new Vector2(3f, 6f), "적궁병");
|
|
Assert.IsTrue(bm.RequestPlayerAction(p, p.Position + new Vector2(0f, 2f)));
|
|
float t0 = Time.realtimeSinceStartup;
|
|
while (bm.Phase != BattlePhase.PlayerTurn && Time.realtimeSinceStartup - t0 < 10f) yield return null;
|
|
Assert.AreEqual(BattlePhase.PlayerTurn, bm.Phase, "버튼 없이 모든 유닛 행동 → 적군 턴 → 아군 턴");
|
|
Assert.AreEqual(2, bm.TurnNumber);
|
|
Assert.IsTrue(e.IsAlive);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Win_WhenAllEnemiesDead()
|
|
{
|
|
yield return Fresh();
|
|
var p = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -3f), "아군");
|
|
var e = bm.SpawnUnit(UnitType.Archer, Faction.Enemy, new Vector2(0f, -1f), "적궁병");
|
|
e.Hp = 1;
|
|
Assert.IsTrue(bm.RequestPlayerAction(p, e.Position));
|
|
float t0 = Time.realtimeSinceStartup;
|
|
while (bm.Phase == BattlePhase.Resolving && Time.realtimeSinceStartup - t0 < 10f) yield return null;
|
|
Assert.AreEqual(BattlePhase.StageClear, bm.Phase);
|
|
Assert.IsFalse(e.IsAlive);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator EnemyAI_HoldsOutsideAggroRange_ActsInside()
|
|
{
|
|
yield return Fresh();
|
|
var p = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -13f), "아군");
|
|
var far = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 10f), "먼적");
|
|
var near = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, -5f), "가까운적");
|
|
var players = bm.AliveUnits(Faction.Player);
|
|
Assert.IsFalse(EnemyAI.Decide(far, players, 1f, 11f, bm.Rng, bm.Board).HasValue, "교전 범위 밖 → 대기");
|
|
Assert.IsTrue(EnemyAI.Decide(near, players, 1f, 11f, bm.Rng, bm.Board).HasValue, "교전 범위 안 → 행동");
|
|
Assert.IsTrue(p.IsAlive);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Camera_BoardWiderThanView_ClampsAndPans()
|
|
{
|
|
var cc = Camera.main.GetComponent<CameraController>();
|
|
Assert.IsNotNull(cc, "넓은 전장 카메라 컨트롤러");
|
|
Assert.Greater(bm.Config.boardSize.y, bm.Config.cameraViewHeight, "전장이 화면보다 넓어야 스크롤이 의미 있다");
|
|
var r = bm.Board.Rect;
|
|
cc.SnapTo(new Vector2(1000f, 1000f));
|
|
var v = cc.ViewRect;
|
|
Assert.LessOrEqual(v.yMax, r.yMax + cc.Margin + 0.01f, "전장 위쪽 밖으로 못 나감");
|
|
Assert.LessOrEqual(v.xMax, r.xMax + cc.Margin + 0.01f + Mathf.Max(0f, v.width - r.width), "전장 오른쪽 밖으로 못 나감");
|
|
cc.SnapTo(Vector2.zero);
|
|
cc.PanBy(new Vector2(0f, 2f));
|
|
Assert.AreEqual(2f, cc.transform.position.y, 0.01f, "빈 곳 드래그 스크롤");
|
|
yield return null;
|
|
}
|
|
}
|
|
}
|