From 5dcbce60192cb94689bcc6d952258e98f649c202 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 29 Jul 2026 11:57:57 +0800 Subject: [PATCH] =?UTF-8?q?docs(enemy):=20=E8=AE=A1=E5=88=92=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20T5b=E2=80=94=E2=80=94BrainBuilder.DeclareState=20?= =?UTF-8?q?=E9=98=B2=E6=A8=A1=E5=9D=97=E9=97=B4=E7=8A=B6=E6=80=81=E5=90=8D?= =?UTF-8?q?=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 契约质量审查提出:模块化后 State() 的"有则取"语义使两个独立模块 取同名状态时后者静默覆盖前者回调。与 T1 的 RequireState 是同一类 失败的两面,趁 0 个模块实现时补代价最低。附带补充交战/死亡层 "为何单阶段 Build 足够"的不变量说明。 Co-Authored-By: Claude Opus 5 (1M context) --- .../2026-07-29-enemy-ai-composable-modules.md | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/Docs_Dev/superpowers/plans/2026-07-29-enemy-ai-composable-modules.md b/Docs_Dev/superpowers/plans/2026-07-29-enemy-ai-composable-modules.md index 2ab5763d..f754b696 100644 --- a/Docs_Dev/superpowers/plans/2026-07-29-enemy-ai-composable-modules.md +++ b/Docs_Dev/superpowers/plans/2026-07-29-enemy-ai-composable-modules.md @@ -680,6 +680,130 @@ git commit -m "feat(enemy): 未发现层/交战层/死亡层三个模块契约" --- +## Task 5b: `BrainBuilder.DeclareState` 防状态名冲突 + +> 本任务是执行 T5 后由契约质量审查提出并采纳的追加项,非原稿内容。 +> +> **风险来由**:`BrainBuilder.State(name)` 是「有则取、无则建」。改造前 `PerceptionStateMachine` +> 是单个函数独占全部状态名,撞名不可能发生。改成「策划在下拉里自由组合三层独立模块」之后, +> 两个不同作者的模块取了同一个状态名(或某模块内部态叫 `Alert`,撞上骨架保留的那个), +> 后声明者的 `.OnEnter/.Tick/.OnExit` 会**静默覆盖**前者 —— 敌人跑错行为且零报错。 +> +> 这与 T1 的 `RequireState` 是同一类失败的两面:`RequireState` 守「挂边到未声明的态」, +> `DeclareState` 守「重复声明同一个态」。趁只有 0 个模块实现时补,代价最低。 + +**Files:** +- Modify: `Assets/_Game/Scripts/AI/BrainBuilder.cs` +- Modify: `Assets/_Game/Scripts/Enemies/AIBrain/Modules/AiStateFragments.cs` +- Modify: `Assets/_Game/Scripts/Enemies/AIBrain/Modules/IEngagementModule.cs` +- Modify: `Assets/_Game/Scripts/Enemies/AIBrain/Modules/IDeathModule.cs` +- Test: `Assets/Tests/EditMode/AI/BrainBuilderTests.cs` + +- [ ] **Step 1: 写失败测试** + +在 `BrainBuilderTests.cs` 类体内追加: + +```csharp + [Test] + public void DeclareState_Throws_OnDuplicateName() + { + var b = new BrainBuilder(); + b.DeclareState("Dup"); + var ex = Assert.Throws(() => b.DeclareState("Dup")); + StringAssert.Contains("Dup", ex.Message); + } + + [Test] + public void DeclareState_ReturnsUsableBuilder_OnFirstDeclare() + { + var b = new BrainBuilder(); + b.Entry("A"); + b.DeclareState("A").To("B").When(c => true, "go"); + b.DeclareState("B"); + Assert.DoesNotThrow(() => b.Build()); + } + + [Test] + public void State_StaysLenient_ForAttachingToDeclaredState() + { + // State() 仍是"有则取"——骨架要给模块已声明的态挂升级边,靠的就是这个。 + var b = new BrainBuilder(); + b.Entry("A"); + b.DeclareState("A"); + Assert.DoesNotThrow(() => b.State("A").To("A").When(c => false, "noop")); + } +``` + +- [ ] **Step 2: 运行测试确认失败** + +运行 `BrainBuilderTests`。期望:编译错误 `'BrainBuilder' does not contain a definition for 'DeclareState'`。 + +- [ ] **Step 3: 实现 `DeclareState`** + +在 `BrainBuilder.cs` 的 `RequireState` 之后插入: + +```csharp + /// + /// 首次声明一个状态。名字已存在即抛——防止两个独立模块取了同名状态时, + /// 后者的 OnEnter/Tick/OnExit 静默覆盖前者(敌人跑错行为且零报错)。 + /// 只有"首次声明行为"的调用点用它;给已声明的态挂边仍用 State()。 + /// + public StateBuilder DeclareState(string name) + { + if (_states.ContainsKey(name)) + throw new InvalidOperationException( + $"BrainBuilder: 状态 '{name}' 已被声明过。两个模块取了同名状态会互相覆盖回调——" + + "请给其中一个换个不冲突的名字。"); + return State(name); + } +``` + +- [ ] **Step 4: 让四个建态原语走 `DeclareState`** + +`AiStateFragments.cs` 中,把四个原语里的 `b.State(state)` 全部换成 `b.DeclareState(state)` +(`Locomotion` / `Ability` / `AbilityOnce` / `Terminal` 各一处)。私有辅助方法不动。 + +- [ ] **Step 5: 给交战层 / 死亡层契约补一条不变量说明** + +`IEngagementModule.cs` 的接口 XML 注释末尾追加一段: + +``` + /// 为何本层只需单阶段 Build(而未发现层要拆 Declare/Link):骨架只把边**指向** + /// EntryState,从不在本模块自己的状态上挂边,所以 Build 内声明的边不会被外部抢占。 + /// 将来若修改骨架、使其向交战态挂边,必须先回来重新审视本契约。 +``` + +`IDeathModule.cs` 的接口 XML 注释末尾追加: + +``` + /// 同 IEngagementModule:骨架只把全局 Died 边指向 EntryState,不在死亡链内部挂边, + /// 故单阶段 Build 足够。 +``` + +并在 `IUnawareModule.cs` 的 `Entry` 成员注释上补一句区分: + +``` + /// 图入口(出生态)。注意与交战/死亡层的 EntryState 不同: + /// 本属性会成为整张图唯一的 BrainBuilder.Entry,而 EntryState 只是别处 To() 的目标。 +``` + +- [ ] **Step 6: 运行测试确认通过** + +运行 `BrainBuilderTests` 与 `AiStateFragmentsTests`,再跑全量 EditMode。 +期望:新增 3 个测试,全量从 222 变为 225,0 失败。 + +> 若有既有测试因重复声明而失败,**停下报告** —— 那说明框架里本来就存在重复声明, +> 是真问题,不要靠把 `DeclareState` 改宽松来绕过。 + +- [ ] **Step 7: 提交** + +```bash +git add Assets/_Game/Scripts/AI/BrainBuilder.cs Assets/_Game/Scripts/Enemies/AIBrain/Modules Assets/Tests/EditMode/AI/BrainBuilderTests.cs +git commit -m "feat(ai): BrainBuilder.DeclareState——重复声明状态即报错,防模块间同名静默覆盖" +``` + +--- + ## Task 6: 未发现层三个模块 **Files:**