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);
}
}
}