docs(enemy): 记录 EditMode 不触发 Awake 的坑,并分派两处越界发现

执行须知第 4 条:EditMode 下 AddComponent 不调用 Awake(Task 4 实测证实)。
后果是 EnemyAbilityBase._enemy 留空使 CanUse 恒假——依赖它的断言在修复前
也会通过,成为永远不会失败的测试。做法是给测试子类加 RunAwake() 显式触发,
不要反射直写字段。

Task 5 追加:IAttackCandidate 的 CanUse 注释补上 enabled 这一维。
Task 21 追加:EnemyQuotaManager._maxActiveBehaviorTrees 改名须带
FormerlySerializedAs,否则丢场景配置值。
This commit is contained in:
2026-07-30 13:30:44 +08:00
parent 64d2087adc
commit 24c612930e
@@ -53,6 +53,16 @@ Unity 只对**编译失败**的程序集给出完整诊断集。`ChargeAbility.c
**3. MCP 测试运行器忽略过滤参数。** `unity_testing_run_tests``testFilter` / `filter` 无效,
每次都跑整个 EditMode 套件。这不影响正确性(等于免费拿到全量回归),但别把全量结果误读成过滤结果。
**4. ⚠️ EditMode 下 `AddComponent` 不触发 `Awake`。**Task 4 实测证实)
普通 MonoBehaviour 在编辑模式下 `AddComponent``Awake` **不会被调用**
后果:任何依赖 `Awake` 解析引用的成员在 EditMode 测试里都是未初始化状态。
具体地,`EnemyAbilityBase._enemy` 会留空,使 `CanUse` **恒为 false**——
断言"禁用后不可用"会在修复前也通过,是个永远不会失败的测试。
做法:给测试用的子类加一个 `public void RunAwake() => Awake();` 显式触发,
**不要**用反射直写字段——前者跑的是真实的 `GetComponentInParent` 解析路径,后者是伪造结果。
只检查 `enabled` 标志本身(如阶段门的启停断言)则不需要 `Awake`
### 项目硬约束
1. **禁止下游兜底**CLAUDE.md 第 6 条):不要用 `?? 默认值` / `try-catch` / `if(null) return` 掩盖上游漏配。缺配置就 `Debug.LogError` 或抛异常。
@@ -729,9 +739,15 @@ namespace BaseGames.Tests.EditMode.Enemies
[Test]
public void DisabledAbility_IsNotUsable()
{
// 必须先挂 EnemyBase:否则 _enemy==null 会让 CanUse 恒假,
// 这条断言在修复前也会"通过",成为一个永远不会失败的测试。
var go = new GameObject("ability-host");
go.AddComponent<EnemyBase>(); // 裸 EnemyBase 的 _currentState=Controlled → IsAlive 为真
var ab = go.AddComponent<StubAbility>();
Assert.IsTrue(ab.CanUse,
"前提:启用且宿主存活时应可用——否则下一条断言分不清是 enabled 还是别的门在起作用");
ab.enabled = false;
Assert.IsFalse(ab.CanUse, "禁用的能力组件不可用——否则选招器会选中一个启不动协程的招");
@@ -741,14 +757,15 @@ namespace BaseGames.Tests.EditMode.Enemies
}
```
> `using BaseGames.Enemies;` 需一并加入 using 区(`EnemyBase` 在该命名空间)。
> `EnemyBase.Awake` 末尾的 `_stats.Initialize(_statsSO)` 会在裸物体上 NRE
> 但那发生在所有组件解析之后,不影响本测试;`LogAssert.ignoreFailingMessages` 已覆盖噪声。
- [ ] **Step 2: 跑测试确认失败**
Test Runner → EditMode → `BossPhaseAbilityGateTests.DisabledAbility_IsNotUsable`
期望:FAIL——`CanUse` 当前不看 `enabled`,返回 true。
> 若因 `StubAbility` 上无 `EnemyBase` 导致 `_enemy == null` 使 `CanUse` 已为 false 而测试假通过:
> 给 `go` 先 `AddComponent<EnemyBase>()`,再断言 `ab.enabled = true` 时 `CanUse` 为 true、
> `false` 时为 false 的对比,确保断言真正测到 `enabled` 这一维。
期望:**第一条断言通过、第二条 FAIL**——`CanUse` 当前不看 `enabled`禁用后仍返回 true。
若两条都过,说明测试没测到 `enabled` 这一维,停下来修测试再继续。
- [ ] **Step 3: `CanUse` 纳入 `enabled`**
@@ -792,7 +809,15 @@ StartCoroutine 必然失败的招。这也是阶段门(按阶段启停能力
**Files:**
- Create: `Assets/_Game/Scripts/Enemies/Boss/BossPhaseAbilityGate.cs`
- Modify: `Assets/_Game/Scripts/Enemies/Boss/BossBase.cs`
- Test: `Assets/Tests/EditMode/Enemies/BossPhaseAbilityGateTests.cs`
- Modify: `Assets/_Game/Scripts/Enemies/Abilities/IAttackCandidate.cs`(一行文档修正,见下)
- Test: `Assets/Tests/EditMode/Enemies/BossPhaseAbilityGateTests.cs`Task 4 已建,本 Task 追加三条)
> **前置说明**`StubAbility` 与 `_host` / `TearDown` 骨架在 Task 4 已经建好,含
> `public void RunAwake() => Awake();`EditMode 不自动跑 `Awake`,见执行须知第 4 条)。
> 本 Task 的三条测试只断言 `enabled` 标志本身,**不需要** `RunAwake`。
>
> **顺带修正**`IAttackCandidate.cs` 第 9 行把 `CanUse` 注释为 `// 未冷却 + 未运行 + 存活`,
> Task 4 加了 `enabled` 这一维后该注释已不完整。改为 `// 组件启用 + 未冷却 + 未运行 + 存活`。
- [ ] **Step 1: 写失败测试**
@@ -2838,6 +2863,11 @@ grep -rn "BossSkill\|WeakPoint\|Telegraph\|AttackPattern\|SkillSequence\|StopBeh
在各自任务里改到)。**本步负责清掉剩余的**:逐个改为只描述功能的表述,不删除功能性内容。
清完后再跑一次 grep,此时才应无命中。
**已点名的一处需特别处理**`EnemyQuotaManager` 有序列化字段 `_maxActiveBehaviorTrees`
与注释 `等价于旧的禁用行为树`(Task 4 执行时发现)。字段改名会丢已有场景/预制体上的配置值,
必须加 `[UnityEngine.Serialization.FormerlySerializedAs("_maxActiveBehaviorTrees")]`
再改为 `_maxActiveBrains`(该管理器实际裁剪的是 `EnemyAiBrain`,不是能力组件)。
- [ ] **Step 3: 更新指导手册**
`Docs/Guides/` 的敌人 / Boss 章节新增一节「Boss AI 怎么写」,内容覆盖: