ThreeKingdomsSlingshot/Assets/Tests/PlayMode/SkillTests.cs

596 lines
25 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using TKS.OutGame;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
namespace TKS.Tests
{
/// <summary>
/// 스킬(2026-11-10 #1112 · PD 엑셀 「장수 목록」 새 기획 · 효과 표 skill_effects.csv) — 효과 묶음별 동작:
/// 표 무결 · 진영 버프·부활 · 즉시 광역(피해·밀기·출혈 / 피해 없는 기절) · 재행동·반격 면역 · 무조건 치명타·흡혈 · 중독 중첩 · 혼란 · 출혈 · 무적 ·
/// 호위 · 기병 복귀 · 거대 화살 · 화염구(화상) · 회복·정화 · 위치 교환 · 아군 통과·탭 밀치기 · 보병 관통 · 가상 실행 복원.
/// 기대값은 효과 표 수치(SkillCatalog · Eff)와 자산 값에서 읽는다(C45 · 테스트에 수치 상수 없음 · 스킬 id 만 지정).
/// </summary>
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<Unit, float> { { 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);
}
}
}