diff --git a/Docs_Dev/superpowers/plans/2026-07-30-boss-ai-single-track.md b/Docs_Dev/superpowers/plans/2026-07-30-boss-ai-single-track.md index ba70d14a..347ee848 100644 --- a/Docs_Dev/superpowers/plans/2026-07-30-boss-ai-single-track.md +++ b/Docs_Dev/superpowers/plans/2026-07-30-boss-ai-single-track.md @@ -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,8 +739,14 @@ namespace BaseGames.Tests.EditMode.Enemies [Test] public void DisabledAbility_IsNotUsable() { - var go = new GameObject("ability-host"); - var ab = go.AddComponent(); + // 必须先挂 EnemyBase:否则 _enemy==null 会让 CanUse 恒假, + // 这条断言在修复前也会"通过",成为一个永远不会失败的测试。 + var go = new GameObject("ability-host"); + go.AddComponent(); // 裸 EnemyBase 的 _currentState=Controlled → IsAlive 为真 + var ab = go.AddComponent(); + + 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()`,再断言 `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 怎么写」,内容覆盖: