按阶段启停能力组件,被禁用的招经 CanUse 自动退出选招候选, 阶段门不需要碰选招器。取代 BossSkillSO.availablePhaseIndices。 EnterPhase 先换池后广播;OnSpawn 回阶段 0(对象池复用)。
126 lines
5.6 KiB
C#
126 lines
5.6 KiB
C#
using System.Collections;
|
||
using System.Reflection;
|
||
using NUnit.Framework;
|
||
using UnityEngine;
|
||
using UnityEngine.TestTools;
|
||
using BaseGames.Enemies;
|
||
using BaseGames.Enemies.Abilities;
|
||
|
||
namespace BaseGames.Tests.EditMode.Enemies
|
||
{
|
||
public class BossPhaseAbilityGateTests
|
||
{
|
||
/// <summary>最小可实例化能力:不播动画、不碰 HitBox,只为验证启停与 CanUse。</summary>
|
||
private sealed class StubAbility : EnemyAbilityBase
|
||
{
|
||
/// <summary>
|
||
/// 编辑模式下 Unity 不会在 AddComponent 时调用 Awake,_enemy 会留空导致 CanUse 恒假。
|
||
/// 这里手动跑一次真实的 Awake(而非反射直写字段),让依赖解析走生产代码路径。
|
||
/// </summary>
|
||
public void RunAwake() => Awake();
|
||
|
||
protected override IEnumerator ExecuteCoroutine() { yield break; }
|
||
}
|
||
|
||
/// <summary>测试期建出的宿主对象;在 TearDown 统一销毁,断言失败也不会残留到编辑器场景。</summary>
|
||
private GameObject _host;
|
||
|
||
// RunAwake 时 EnemyAbilityBase 找不到 AnimancerComponent 会告警,与本测试无关。
|
||
[SetUp]
|
||
public void SetUp() => LogAssert.ignoreFailingMessages = true;
|
||
|
||
[TearDown]
|
||
public void TearDown()
|
||
{
|
||
// 项目关闭了 Domain/Scene Reload:断言失败会跳过用例后续语句,
|
||
// 清理必须放在 TearDown,否则失败一次就往编辑器场景里漏一个对象。
|
||
if (_host != null) Object.DestroyImmediate(_host);
|
||
_host = null;
|
||
LogAssert.ignoreFailingMessages = false;
|
||
}
|
||
|
||
[Test]
|
||
public void DisabledAbility_IsNotUsable()
|
||
{
|
||
// 必须先挂 EnemyBase:否则 _enemy==null 会让 CanUse 恒假,
|
||
// 这条断言在修复前也会"通过",成为一个永远不会失败的测试。
|
||
_host = new GameObject("ability-host");
|
||
_host.AddComponent<EnemyBase>(); // 裸 EnemyBase 的 _currentState=Controlled → IsAlive 为真
|
||
var ab = _host.AddComponent<StubAbility>();
|
||
ab.RunAwake(); // 编辑模式不自动跑 Awake,手动解析 _enemy
|
||
|
||
Assert.IsTrue(ab.CanUse,
|
||
"前提:启用且宿主存活时应可用——否则下一条断言分不清是 enabled 还是别的门在起作用");
|
||
|
||
ab.enabled = false;
|
||
Assert.IsFalse(ab.CanUse, "禁用的能力组件不可用——否则选招器会选中一个启不动招的招");
|
||
}
|
||
|
||
// 阶段门的 _entries 是私有序列化字段——与放置脚手架同样用反射写入。
|
||
// 反射只用来喂入这份配置数据,ApplyPhase 的判定逻辑走的仍是生产代码。
|
||
private static void SetEntries(BossPhaseAbilityGate gate,
|
||
params (EnemyAbilityBase ab, int[] phases)[] items)
|
||
{
|
||
var arr = new BossPhaseAbilityGate.PhaseEntry[items.Length];
|
||
for (int i = 0; i < items.Length; i++)
|
||
arr[i] = new BossPhaseAbilityGate.PhaseEntry { ability = items[i].ab, phases = items[i].phases };
|
||
typeof(BossPhaseAbilityGate)
|
||
.GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance)
|
||
.SetValue(gate, arr);
|
||
}
|
||
|
||
[Test]
|
||
public void ApplyPhase_EnablesOnlyAbilitiesAllowedInThatPhase()
|
||
{
|
||
_host = new GameObject("boss");
|
||
var gate = _host.AddComponent<BossPhaseAbilityGate>();
|
||
var p0 = _host.AddComponent<StubAbility>();
|
||
// EnemyAbilityBase 是 [DisallowMultipleComponent],第二个能力必须另起子节点
|
||
// (挂在 _host 下,TearDown 会连带销毁)
|
||
var p1Go = new GameObject("p1"); p1Go.transform.SetParent(_host.transform);
|
||
var p1 = p1Go.AddComponent<StubAbility>();
|
||
|
||
SetEntries(gate, (p0, new[] { 0 }), (p1, new[] { 1 }));
|
||
|
||
gate.ApplyPhase(0);
|
||
Assert.IsTrue (p0.enabled, "阶段 0 的招应在阶段 0 启用");
|
||
Assert.IsFalse(p1.enabled, "阶段 1 的招不应在阶段 0 启用");
|
||
|
||
gate.ApplyPhase(1);
|
||
Assert.IsFalse(p0.enabled, "换阶段后旧池的招应被禁用");
|
||
Assert.IsTrue (p1.enabled, "换阶段后新池的招应被启用");
|
||
}
|
||
|
||
[Test]
|
||
public void ApplyPhase_EmptyPhaseArrayMeansAllPhases()
|
||
{
|
||
_host = new GameObject("boss");
|
||
var gate = _host.AddComponent<BossPhaseAbilityGate>();
|
||
var any = _host.AddComponent<StubAbility>();
|
||
|
||
SetEntries(gate, (any, new int[0]));
|
||
|
||
gate.ApplyPhase(0);
|
||
Assert.IsTrue(any.enabled, "空阶段数组 = 全阶段可用");
|
||
gate.ApplyPhase(7);
|
||
Assert.IsTrue(any.enabled, "空阶段数组在任意阶段都应保持启用");
|
||
}
|
||
|
||
[Test]
|
||
public void ApplyPhase_UnlistedAbilityIsUntouched()
|
||
{
|
||
_host = new GameObject("boss");
|
||
var gate = _host.AddComponent<BossPhaseAbilityGate>();
|
||
var listed = _host.AddComponent<StubAbility>();
|
||
var otherGo = new GameObject("unlisted"); otherGo.transform.SetParent(_host.transform);
|
||
var unlisted = otherGo.AddComponent<StubAbility>();
|
||
|
||
SetEntries(gate, (listed, new[] { 1 }));
|
||
|
||
gate.ApplyPhase(0);
|
||
Assert.IsFalse(listed.enabled, "登记的能力应被阶段门管控");
|
||
Assert.IsTrue (unlisted.enabled, "未登记的能力不受阶段门影响");
|
||
}
|
||
}
|
||
}
|