using System.Collections; using System.Collections.Generic; using NUnit.Framework; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TestTools; namespace TKS.Tests { /// 병종 4종 규칙·턴 흐름·승리 판정을 실제 씬에서 검증한다(헤드리스 실행 가능). 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"); 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 { 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 { 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); } } }