using System.Collections; using System.Collections.Generic; using NUnit.Framework; using TKS.OutGame; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TestTools; namespace TKS.Tests { /// /// 스킬(2026-11-10 #1112 · PD 엑셀 「장수 목록」 새 기획 · 효과 표 skill_effects.csv) — 효과 묶음별 동작: /// 표 무결 · 진영 버프·부활 · 즉시 광역(피해·밀기·출혈 / 피해 없는 기절) · 재행동·반격 면역 · 무조건 치명타·흡혈 · 중독 중첩 · 혼란 · 출혈 · 무적 · /// 호위 · 기병 복귀 · 거대 화살 · 화염구(화상) · 회복·정화 · 위치 교환 · 아군 통과·탭 밀치기 · 보병 관통 · 가상 실행 복원. /// 기대값은 효과 표 수치(SkillCatalog · Eff)와 자산 값에서 읽는다(C45 · 테스트에 수치 상수 없음 · 스킬 id 만 지정). /// public class SkillTests { BattleManager bm; BalanceConfig cfg; [UnitySetUp] public IEnumerator SetUp() { WorldProgress.Reset(); SaveStore.UseMemory(true); SceneManager.LoadScene("Battle"); yield return null; yield return null; bm = BattleManager.Instance; Assert.IsNotNull(bm); var wm = WorldMapScreen.Instance; if (wm != null && wm.HasData) wm.EnterStage(0, 0); yield return null; cfg = bm.Config; SkillSystem.AutoTap = true; Time.timeScale = 20f; } [UnityTearDown] public IEnumerator TearDown() { SkillSystem.AutoTap = false; Time.timeScale = 1f; yield return null; } IEnumerator Fresh() { bm.StopAllCoroutines(); bm.ClearUnits(); bm.ForcePhase(BattlePhase.PlayerTurn); bm.SetTurnForTest(1); yield return null; } // ---------- 도우미 ---------- static SkillEffect Eff(string skillId, string key) { var d = SkillCatalog.Get(skillId); Assert.IsNotNull(d, skillId); foreach (var e in d.ScaledEffects(1, 1f)) if (e.Key == key) return e; Assert.Fail(skillId + " 에 " + key + " 없음"); return null; } Unit Spawn(UnitType t, Faction f, float x, float y, string name) { return bm.SpawnUnit(t, f, new Vector2(x, y), name, 1f, 1f, false); } UnitSkills Give(Unit u, string heroId) { var s = bm.Skills.AttachHero(u, heroId); Assert.IsNotNull(s, heroId); return s; } void Begin() { bm.Skills.BeginBattle(); } IEnumerator Act(Unit u, Vector2 target, ActionOutcome o) { yield return bm.ResolveForTest(u, target, false, o); } IEnumerator ActArmed(Unit u, Vector2 target, ActionOutcome o) { bm.Skills.Of(u).Reserved = true; yield return bm.ResolveForTest(u, target, false, o); } // ---------- 표 ---------- [UnityTest] public IEnumerator Data_EveryHeroSkillHasEffects() { yield return Fresh(); var db = OutGameData.Default; Assert.IsNotNull(db); int skills = 0; foreach (var h in db.Heroes.heroes) { if (h.isSoldier) continue; foreach (var sid in new[] { h.skillActive, h.skillPassive }) { var d = SkillCatalog.Get(sid); Assert.IsNotNull(d, sid); Assert.Greater(d.Effects.Count, 0, sid + " 효과 줄"); foreach (var e in d.Effects) Assert.IsTrue(SkillKeys.Known(e.Key), sid + " " + e.Key); skills++; } } Assert.AreEqual(db.Skills.skills.Count, skills); Assert.IsTrue(db.Heroes.Get("diaochan").Female); Assert.IsFalse(db.Heroes.Get("lvbu").Female); } // ---------- 패시브: 진영 공격 · 부활 ---------- [UnityTest] public IEnumerator Passive_CampAttack_And_Revive() { yield return Fresh(); var liu = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "유비"); Give(liu, "liubei"); var shu = Spawn(UnitType.Spear, Faction.Player, 2f, -3f, "촉"); Give(shu, "guanyu"); var wei = Spawn(UnitType.Spear, Faction.Player, 4f, -3f, "위"); Give(wei, "zhanghe"); Begin(); float camp = Eff("liubei_p", SkillKeys.AtkPct).P; Assert.AreEqual(1f + camp, SkillMods.AttackMult(liu), 1e-4f, "촉 진영 공격 +"); Assert.AreEqual(1f, SkillMods.AttackMult(wei), 1e-4f, "위 진영은 안 받음"); var rv = Eff("liubei_p", SkillKeys.Revive); int got = bm.Skills.Hit(liu, liu.Hp + 50, null, HitKind.Dot); Assert.IsTrue(liu.IsAlive, "부활"); Assert.AreEqual(Mathf.RoundToInt(liu.MaxHp * rv.P), liu.Hp); Assert.Greater(got, 0); bm.Skills.Hit(liu, liu.Hp + 50, null, HitKind.Dot); Assert.IsFalse(liu.IsAlive, "부활은 전투당 " + rv.Get(1) + "회"); } // ---------- 즉시 광역 ---------- [UnityTest] public IEnumerator Instant_AoeStrike_DamagePushBleed() { yield return Fresh(); var gy = Spawn(UnitType.Spear, Faction.Player, 0f, 0f, "관우"); var s = Give(gy, "guanyu"); var aoe = Eff("guanyu_a", SkillKeys.AoeStrike); var near = Spawn(UnitType.Infantry, Faction.Enemy, aoe.P * 0.5f, 0f, "근처"); var far = Spawn(UnitType.Infantry, Faction.Enemy, aoe.P + 3f, 0f, "멀리"); Begin(); int hp0 = near.Hp, farHp = far.Hp; float d0 = Vector2.Distance(gy.Position, near.Position); yield return bm.Skills.Instant(s); yield return new WaitForSeconds(0.3f); Assert.Less(near.Hp, hp0, "반경 안 피해"); Assert.AreEqual(farHp, far.Hp, "반경 밖 무사"); Assert.Greater(Vector2.Distance(gy.Position, near.Position), d0 + 0.05f, "바깥으로 밀림"); Assert.IsTrue(SkillStatus.Bleeding(near), "출혈"); Assert.AreEqual(1 + s.Active.Cooldown, s.ReadyTurn, "쿨"); } [UnityTest] public IEnumerator Instant_AoeStun_NoDamage() { yield return Fresh(); var zf = Spawn(UnitType.Cavalry, Faction.Player, 0f, 0f, "장비"); var s = Give(zf, "zhangfei"); var aoe = Eff("zhangfei_a", SkillKeys.AoeStrike); var foe = Spawn(UnitType.Infantry, Faction.Enemy, aoe.P * 0.5f, 0f, "적"); Begin(); int hp0 = foe.Hp; yield return bm.Skills.Instant(s); Assert.AreEqual(hp0, foe.Hp, "피해 없음(p2 0)"); Assert.IsTrue(SkillStatus.Stunned(foe), "기절"); } // ---------- 다음 행동: 재행동 · 반격 면역 ---------- [UnityTest] public IEnumerator NextAction_ExtraAction_CounterImmune() { yield return Fresh(); var liu = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "유비"); var s = Give(liu, "liubei"); var foe = Spawn(UnitType.Spear, Faction.Enemy, 0f, 0f, "적"); Begin(); var o = new ActionOutcome(); yield return ActArmed(liu, new Vector2(0f, 1f), o); Assert.AreEqual(0, o.CounterTaken, "반격 안 받음"); Assert.AreEqual(Mathf.RoundToInt(Eff("liubei_a", SkillKeys.ExtraAction).P), s.ReactCredits, "재행동"); Assert.IsTrue(bm.Skills.TakeReaction(liu)); Assert.AreEqual(0f, SkillMods.CounterTakenMult(liu, foe), 1e-4f, "1턴 반격 면역은 재행동에도"); } // ---------- 치명타 · 흡혈 ---------- [UnityTest] public IEnumerator CritAlways_And_Lifesteal() { yield return Fresh(); var mc = Spawn(UnitType.Cavalry, Faction.Player, 0f, -3f, "마초"); var s = Give(mc, "machao"); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 0f, "적"); Begin(); s.Armed = true; Combat.Damage(cfg, mc, foe, false, new System.Random(1), true); Assert.IsTrue(Combat.LastCrit, "무조건 치명타"); s.Armed = false; mc.Hp = mc.MaxHp / 2; int before = mc.Hp; int dealt = bm.Skills.Hit(foe, 40, mc, HitKind.Attack); Assert.AreEqual(Mathf.RoundToInt(dealt * Eff("machao_p", SkillKeys.Lifesteal).P), mc.Hp - before, "흡혈"); } // ---------- 상태 이상 ---------- [UnityTest] public IEnumerator Poison_Stacks_And_Ticks() { yield return Fresh(); var jx = Spawn(UnitType.Strategist, Faction.Player, 0f, -3f, "가후"); Give(jx, "jiaxu"); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 3f, "적"); Begin(); bm.Skills.ApplyHitStatuses(jx, foe, false, HitKind.Attack); bm.Skills.ApplyHitStatuses(jx, foe, false, HitKind.Attack); var st = bm.Skills.StatusOf(foe); Assert.AreEqual(2, st.PoisonStacks, "중첩"); var hp = Eff("jiaxu_p", SkillKeys.HitPoison); int before = foe.Hp; yield return bm.Skills.TurnBegin(Faction.Enemy); Assert.AreEqual(Mathf.Max(1, Mathf.RoundToInt(foe.MaxHp * hp.Get(1) * 2)), before - foe.Hp, "턴 시작 중독 × 중첩"); } [UnityTest] public IEnumerator Confuse_HalvesSling_ThenExpires() { yield return Fresh(); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 3f, "적"); Begin(); var c = Eff("zhangliao_a", SkillKeys.HitConfuse); float base0 = SkillMods.SlingMax(foe); bm.Skills.ApplyConfuse(foe, Mathf.RoundToInt(c.P), c.Get(1)); Assert.AreEqual(base0 * c.Get(1), SkillMods.SlingMax(foe), 1e-3f, "혼란 = 이동 × 배율"); yield return bm.Skills.TurnBegin(Faction.Player); // 적 턴 끝 Assert.AreEqual(base0, SkillMods.SlingMax(foe), 1e-3f, "적 턴 1번 뒤 해제"); } [UnityTest] public IEnumerator Bleed_DamageByMovedDistance() { yield return Fresh(); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, 3f, "적"); Begin(); var b = Eff("xuhuang_p", SkillKeys.HitBleed); bm.Skills.ApplyBleedStatus(foe, Mathf.RoundToInt(b.P), b.Get(1)); int before = foe.Hp; float moved = foe.SlingMaxDistance * 0.5f; bm.Skills.ApplyBleed(new Dictionary { { foe, moved } }); Assert.AreEqual(Mathf.Max(1, Mathf.RoundToInt(foe.MaxHp * b.Get(1) * 0.5f)), before - foe.Hp, "출혈 = 거리 비례"); } // ---------- 무적 · 호위 ---------- [UnityTest] public IEnumerator Invuln_Instant_BlocksDamageForRound() { yield return Fresh(); var zj = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "장각"); var s = Give(zj, "zhangjiao"); Begin(); yield return bm.Skills.Instant(s); int hp = zj.Hp; Assert.AreEqual(0, bm.Skills.Hit(zj, 30, null, HitKind.Attack)); Assert.AreEqual(hp, zj.Hp, "무적"); yield return bm.Skills.TurnBegin(Faction.Player); Assert.Greater(bm.Skills.Hit(zj, 30, null, HitKind.Attack), 0, "다음 자기 턴 시작에 끝"); } [UnityTest] public IEnumerator Guard_TakesAllyDamage() { yield return Fresh(); var gp = Spawn(UnitType.Spear, Faction.Player, 0f, -3f, "관평"); Give(gp, "guanping"); var g = Eff("guanping_p", SkillKeys.Guard); var ally = Spawn(UnitType.Archer, Faction.Player, g.P * 0.5f, -3f, "궁병"); var far = Spawn(UnitType.Archer, Faction.Player, g.P + 4f, -3f, "먼 궁병"); Begin(); int a0 = ally.Hp, g0 = gp.Hp, f0 = far.Hp; bm.Skills.Hit(ally, 40, null, HitKind.Attack); Assert.AreEqual(a0 - Mathf.RoundToInt(40 * (1f - Mathf.Clamp01(g.Get(1)))), ally.Hp, "호위 비율만큼 대신"); Assert.Less(gp.Hp, g0, "관평이 받음"); bm.Skills.Hit(far, 40, null, HitKind.Attack); Assert.AreEqual(f0 - 40, far.Hp, "반경 밖 = 그대로"); } // ---------- 기병 복귀 · 보병 관통 · 교환 · 아군 통과 ---------- [UnityTest] public IEnumerator Cavalry_ReturnsToOriginOnHit() { yield return Fresh(); var zy = Spawn(UnitType.Cavalry, Faction.Player, 0f, -4f, "조운"); Give(zy, "zhaoyun"); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "적"); Begin(); Vector2 origin = zy.Position; var o = new ActionOutcome(); yield return Act(zy, origin + Vector2.up * zy.SlingMaxDistance * 0.9f, o); Assert.Contains(foe, o.Attacked); Assert.IsTrue(o.Returned, "복귀"); Assert.Less(Vector2.Distance(zy.Position, origin), 0.3f, "출발 위치"); Assert.AreEqual(0, o.CounterTaken, "반격 안 받음"); } [UnityTest] public IEnumerator Infantry_PierceThrough_HitsAllOnPath() { yield return Fresh(); var sj = Spawn(UnitType.Infantry, Faction.Player, 0f, -4f, "손견"); Give(sj, "sunjian"); var a = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -2.5f, "적1"); var b = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "적2"); Begin(); var o = new ActionOutcome(); yield return ActArmed(sj, sj.Position + Vector2.up * sj.SlingMaxDistance * 0.95f, o); Assert.Contains(a, o.Attacked); Assert.Contains(b, o.Attacked); Assert.IsTrue(SkillStatus.Bleeding(a) || !a.IsAlive, "출혈"); Assert.AreEqual(0, bm.Skills.Hit(sj, 30, null, HitKind.Attack), "행동 뒤 1턴 무적"); } [UnityTest] public IEnumerator Swap_OnHit_TargetToOrigin() { yield return Fresh(); var wy = Spawn(UnitType.Infantry, Faction.Player, 0f, -4f, "위연"); var s = Give(wy, "weiyan"); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "적"); Begin(); Vector2 origin = wy.Position; Vector2 foe0 = foe.Position; var o = new ActionOutcome(); yield return ActArmed(wy, foe0, o); Assert.Less(Vector2.Distance(foe.Position, origin), 0.05f, "적 → 출발 위치"); Assert.Less(Vector2.Distance(wy.Position, foe0), 0.05f, "위연 → 적 자리"); Assert.AreEqual(Mathf.RoundToInt(Eff("weiyan_a", SkillKeys.ExtraAction).P), s.ReactCredits, "재행동"); } [UnityTest] public IEnumerator PassAllies_ThenAimPush_NoSelfBounce() { yield return Fresh(); var cc = Spawn(UnitType.Infantry, Faction.Player, 0f, -5f, "조조"); Give(cc, "caocao"); var ally = Spawn(UnitType.Archer, Faction.Player, 0f, -3.2f, "아군"); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1.4f, "적"); Begin(); Vector2 foe0 = foe.Position; var o = new ActionOutcome(); yield return ActArmed(cc, cc.Position + Vector2.up * cc.SlingMaxDistance, o); Assert.Contains(foe, o.Attacked, "아군 통과 → 적 명중"); Assert.IsFalse(o.Pushed.Contains(ally), "아군은 밀지 않음"); Assert.Greater(foe.Position.y, foe0.y + 0.05f, "자동 탭 = 슬링 방향으로 밀침"); } // ---------- 원거리 특수 사격 ---------- [UnityTest] public IEnumerator GiantArrow_IgnoresRange_Stuns() { yield return Fresh(); var hz = Spawn(UnitType.Archer, Faction.Player, 0f, -10f, "황충"); Give(hz, "huangzhong"); var ally = Spawn(UnitType.Infantry, Faction.Player, 0f, -7f, "아군"); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, hz.Position.y + SkillMods.Range(hz) * 2f + 2f, "먼 적"); Begin(); int ally0 = ally.Hp, foe0 = foe.Hp; var o = new ActionOutcome(); yield return ActArmed(hz, hz.Position + Vector2.up * 2f, o); Assert.AreEqual(ally0, ally.Hp, "아군 통과"); Assert.Less(foe.Hp, foe0, "사거리 밖 적 명중"); Assert.IsTrue(SkillStatus.Stunned(foe) || !foe.IsAlive, "기절"); } [UnityTest] public IEnumerator Fireball_Blast_Burns() { yield return Fresh(); var xs = Spawn(UnitType.Strategist, Faction.Player, 0f, -4f, "서서"); Give(xs, "xushu"); var bl = Eff("xushu_a", SkillKeys.ShotBlast); var a = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "적1"); var b = Spawn(UnitType.Infantry, Faction.Enemy, bl.P * 0.6f, -1f, "적2"); Begin(); int a0 = a.Hp, b0 = b.Hp; var o = new ActionOutcome(); yield return ActArmed(xs, xs.Position + Vector2.up * 2f, o); Assert.Less(a.Hp, a0); Assert.Less(b.Hp, b0, "폭발 반경"); Assert.Greater(bm.Skills.StatusOf(b).BurnTurns, 0, "화상"); } // ---------- PD 결정 반영(2026-11-10 #1112 · ⑧ ⑩ ⑬ ⑭) ---------- [UnityTest] public IEnumerator Firewall_HitsAlliesToo() { yield return Fresh(); var lx = Spawn(UnitType.Strategist, Faction.Player, 0f, -5f, "육손"); Give(lx, "luxun"); var ally = Spawn(UnitType.Infantry, Faction.Player, 0f, -3f, "아군"); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "적"); Begin(); int a0 = ally.Hp, f0 = foe.Hp; var o = new ActionOutcome(); yield return ActArmed(lx, lx.Position + Vector2.up * 2f, o); Assert.Less(foe.Hp, f0, "적 피해"); Assert.Less(ally.Hp, a0, "아군도 피해(결정 ⑬)"); Assert.Greater(bm.Skills.StatusOf(ally).BurnTurns, 0, "아군도 화상"); Assert.AreEqual(1, bm.Skills.Zones.Count, "장벽 지대"); Assert.IsTrue(bm.Skills.Zones[0].HitsAllies); } [UnityTest] public IEnumerator SplashHeal_HealsTargetWithoutDamage() { yield return Fresh(); var sq = Spawn(UnitType.Strategist, Faction.Player, 0f, -4f, "손건"); Give(sq, "sunqian"); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1f, "적"); Begin(); foe.Hp = foe.MaxHp / 2; int f0 = foe.Hp; var o = new ActionOutcome(); yield return ActArmed(sq, foe.Position, o); Assert.Greater(foe.Hp, f0, "명중 대상도 피해 없이 회복(결정 ⑧)"); Assert.AreEqual(0, o.DamageDealt); } [UnityTest] public IEnumerator Whirl_DealsDamageAndPushes() { yield return Fresh(); var zj = Spawn(UnitType.Strategist, Faction.Player, 0f, -5f, "제갈근"); Give(zj, "zhugejin"); var foe = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -2f, "적"); Begin(); int f0 = foe.Hp; float y0 = foe.Position.y; var o = new ActionOutcome(); yield return ActArmed(zj, zj.Position + Vector2.up * 2f, o); Assert.Less(foe.Hp, f0, "공격력 × p3 피해(결정 ⑭)"); Assert.Greater(foe.Position.y, y0 + 0.5f, "진행 방향으로 밀림"); Assert.IsTrue(SkillStatus.Confused(foe), "혼란"); } [UnityTest] public IEnumerator Zombie_ActsAtNextOwnTurnThenVanishes() { yield return Fresh(); var zl = Spawn(UnitType.Strategist, Faction.Player, 0f, -5f, "장로"); Give(zl, "zhanglu"); var victim = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -2f, "약한 적"); var other = Spawn(UnitType.Infantry, Faction.Enemy, 3f, 1f, "다른 적"); Begin(); victim.Hp = 1; int other0 = other.Hp; var o = new ActionOutcome(); yield return Act(zl, victim.Position, o); Assert.IsFalse(victim.IsAlive); Unit zombie = null; foreach (var u in bm.Units) if (u != null && u.IsAlive && SkillSystem.IsSummon(u)) zombie = u; Assert.IsNotNull(zombie, "망령 생성"); Assert.AreEqual(Faction.Player, zombie.Faction); Assert.AreEqual(other0, other.Hp, "처치 직후에는 공격하지 않음(결정 ⑩)"); yield return bm.Skills.TurnBegin(Faction.Player); Assert.IsFalse(SkillSystem.IsSummon(zombie) && zombie != null && zombie.IsAlive, "자기 턴 시작에 1번 행동 후 소멸"); } // ---------- Fable 검토 지적 수정(2026-11-10 · A1 · A2 · B1) ---------- [UnityTest] public IEnumerator Infantry_SwordWave_PiercesLine() { yield return Fresh(); var zt = Spawn(UnitType.Infantry, Faction.Player, 0f, -5f, "주태"); Give(zt, "zhoutai"); var a = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -3f, "적1"); var b = Spawn(UnitType.Infantry, Faction.Enemy, 0f, -1.5f, "적2"); Begin(); Vector2 start = zt.Position; int a0 = a.Hp, b0 = b.Hp; var o = new ActionOutcome(); yield return ActArmed(zt, zt.Position + Vector2.up * 2f, o); Assert.IsTrue(o.Fired, "보병 검기 = 사격"); Assert.Less(a.Hp, a0); Assert.Less(b.Hp, b0, "직선 위 전원"); Assert.Less(Vector2.Distance(zt.Position, start), 0.05f, "주태는 제자리"); } [UnityTest] public IEnumerator GrantReaction_ToSoldierWithoutSkills() { yield return Fresh(); var gj = Spawn(UnitType.Strategist, Faction.Player, 0f, -3f, "곽가"); var s = Give(gj, "guojia"); var soldier = Spawn(UnitType.Infantry, Faction.Player, 1.5f, -3f, "병사"); Begin(); Assert.IsNull(bm.Skills.Of(soldier), "스킬 없는 병사"); yield return bm.Skills.Instant(s); Assert.IsTrue(bm.Skills.TakeReaction(soldier), "행동 안 한 병사 = 행동 뒤 재행동"); } [UnityTest] public IEnumerator Hot_HealsEveryRoundForDuration() { yield return Fresh(); var pt = Spawn(UnitType.Strategist, Faction.Player, 0f, -3f, "방통"); var s = Give(pt, "pangtong"); var ally = Spawn(UnitType.Infantry, Faction.Player, 1.5f, -3f, "아군"); Begin(); int rounds = Eff("pangtong_a", SkillKeys.Hot).Dur; yield return bm.Skills.Instant(s); int heals = 0; for (int i = 0; i < rounds + 1; i++) { ally.Hp = 1; yield return bm.Skills.TurnBegin(Faction.Player); if (ally.Hp > 1) heals++; } Assert.AreEqual(rounds, heals, "지속 턴 수만큼 회복"); } // ---------- 회복 · 정화 ---------- [UnityTest] public IEnumerator Instant_HealAndCleanse_InRange() { yield return Fresh(); var zg = Spawn(UnitType.Strategist, Faction.Player, 0f, -3f, "제갈량"); var s = Give(zg, "zhugeliang"); var ally = Spawn(UnitType.Infantry, Faction.Player, 1.5f, -3f, "아군"); var far = Spawn(UnitType.Infantry, Faction.Player, zg.SlingMaxDistance + 4f, -3f, "먼 아군"); Begin(); ally.Hp = ally.MaxHp / 3; far.Hp = far.MaxHp / 3; var burn = Eff("xushu_a", SkillKeys.HitBurn); bm.Skills.ApplyBurn(ally, Mathf.RoundToInt(burn.P), burn.Get(1)); int a0 = ally.Hp, f0 = far.Hp; yield return bm.Skills.Instant(s); Assert.Greater(ally.Hp, a0, "범위 안 회복"); Assert.AreEqual(f0, far.Hp, "범위 밖"); Assert.IsFalse(SkillStatus.HasAny(ally), "정화"); } // ---------- 가상 실행(적 AI 예측) 복원 ---------- [UnityTest] public IEnumerator Sim_RestoresSkillState() { yield return Fresh(); var jx = Spawn(UnitType.Strategist, Faction.Enemy, 0f, 3f, "가후"); Give(jx, "jiaxu"); var p = Spawn(UnitType.Infantry, Faction.Player, 0f, 0f, "아군"); Begin(); var snap = Sim.Take(bm); Sim.Active = true; try { var o = new ActionOutcome(); Sim.Run(bm.Resolver.Resolve(jx, p.Position, false, o)); } finally { Sim.Active = false; Sim.Restore(bm, snap); } Assert.IsFalse(SkillStatus.HasAny(p), "가상 실행 중 걸린 중독이 남지 않음"); Assert.AreEqual(p.MaxHp, p.Hp); } } }