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 f754b696..a5c17c3f 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
@@ -4,7 +4,7 @@
**Goal:** 用「固定感知骨架 + 可插拔的 未发现层 / 交战层 / 死亡层」替换 `PerceptionStateMachine`,并让约 95% 的敌人 AI 成为配方资产而非 C# 类。
-**Architecture:** 三个模块契约(`IUnawareModule` / `IEngagementModule` / `IDeathModule`)各自声明自己的状态与边;`PerceptionSkeleton` 只负责把「未发现 → 警觉 → 交战」的升降级边挂到模块声明的态上。模块是 `[Serializable]` 普通类,既能在 C# 里 `new`,也能经 `[SerializeReference]` 存进 `PerceptionRecipeSO` 配方资产。`EnemyAiBrain` 上「配方资产引用」与「AiScript 定义 id」二选一。
+**Architecture:** 两个模块契约(`IUnawareModule` / `IEngagementModule`)各自声明自己的状态与边;`PerceptionSkeleton` 只负责把「未发现 → 警觉 → 交战」的升降级边挂到模块声明的态上,并自行声明 `Death` 终态(死亡归物理层,不做成可插拔层——见 Task 9R)。模块是 `[Serializable]` 普通类,既能在 C# 里 `new`,也能经 `[SerializeReference]` 存进 `PerceptionRecipeSO` 配方资产。`EnemyAiBrain` 上「配方资产引用」与「AiScript 定义 id」二选一。
**Tech Stack:** Unity / C# 9 / NUnit EditMode / 自研 BrainGraph(`BaseGames.AI`)
@@ -60,17 +60,15 @@ Assets/_Game/Scripts/AI/ 框架层(只依赖 Base
Assets/_Game/Scripts/Enemies/AIBrain/Modules/ 敌人决策组合层
AiStateFragments.cs 建态原语(4 个)
- IUnawareModule.cs / IEngagementModule.cs / IDeathModule.cs
- PerceptionSkeleton.cs 感知三层规则
+ IUnawareModule.cs / IEngagementModule.cs
+ PerceptionSkeleton.cs 感知规则 + 自声明 Death 终态
Unaware/SinglePost.cs
Unaware/DisguiseThenPatrol.cs
Unaware/AlternatingIdlePatrol.cs
Engagement/RushExit.cs
Engagement/RushEngagement.cs
Engagement/ApproachAttackEngagement.cs
- Death/TerminalDeath.cs
- Death/AbilityDeath.cs
- Death/TwoStageDeath.cs
+ (无 Death/ 目录:死亡归物理层,见 Task 9R)
Assets/_Game/Scripts/Enemies/AIBrain/Recipes/
PerceptionRecipeSO.cs 一个 SO 类型覆盖所有感知型敌人
@@ -1408,7 +1406,24 @@ git commit -m "feat(enemy): ApproachAttackEngagement——攻击改为起手即
---
-## Task 9: 死亡层三个模块
+## Task 9: ~~死亡层三个模块~~ → 已作废,改为 Task 9R
+
+> **本任务已作废。** 三个死亡模块曾按下方原稿实现并提交(`3bc7eb1`、`5a9c1fd`),
+> 但质量审查发现 `TwoStageDeath` 在真实运行中必然卡死,追根后确认整个死亡层放错了层。
+> 详见 spec 的「修订:死亡层取消」一节与 commit `023a4a4`。
+>
+> **必然卡死的机制**:`EnemyBase.PerformDeath` 先 `ForceState(EnemyStateType.Dead)`(终态)
+> 再 `_brain.Send(AiSignal.Died)`。`IsControllable` 是 `CurrentState == Controlled`,此后永久为假。
+> `AiRuntime.Tick` 的顺序是「事件转换(不受门限制)→ IsControllable 让位门(为假即 return)→
+> 条件转换」,所以 `Died` 事件能切进死亡链,但**此后任何条件边都不再被求值**,
+> `TwoStageDeath` 的 `preDone` 边永不触发。
+>
+> `AbilityDeath` 同样不成立:死亡态 `OnEnter` 执行时,`InterruptAll(Dead)` 已跑过、
+> 碰撞体已关、`Dead` 动画已在播,此刻触发能力是在和 `PerformDeath` 的死亡演出打架。
+>
+> **由 Task 9R 取代。** 下方原稿仅作历史记录保留,不要照它实施。
+
+原稿(已作废,勿实施)
**Files:**
- Create: `Assets/_Game/Scripts/Enemies/AIBrain/Modules/Death/TerminalDeath.cs`
@@ -1447,10 +1462,10 @@ namespace BaseGames.Tests.EditMode.AI
var rt = RunWithDeath(new TerminalDeath(), ctx);
rt.Send(AiSignal.Died);
rt.Tick(0.1f);
- Assert.AreEqual(TerminalDeath.Death, rt.CurrentStateName);
+ Assert.AreEqual(PerceptionSkeleton.Death, rt.CurrentStateName);
Assert.AreEqual(0, ctx.C.Used.Count); // 演出走物理状态机,AI 不触发能力
rt.Tick(0.1f);
- Assert.AreEqual(TerminalDeath.Death, rt.CurrentStateName);
+ Assert.AreEqual(PerceptionSkeleton.Death, rt.CurrentStateName);
}
[Test]
@@ -1607,6 +1622,49 @@ git add Assets/_Game/Scripts/Enemies/AIBrain/Modules/Death Assets/Tests/EditMode
git commit -m "feat(enemy): 死亡层三模块(终态/单段/两段)"
```
+
+
+---
+
+## Task 9R: 收掉死亡层,死亡归物理层
+
+**为什么**:见上方 Task 9 的作废说明与 spec 的「修订:死亡层取消」。要点是——死亡在本项目里
+完全归物理层(`PerformDeath` 切终态/清效果/关碰撞/播死亡动画/归还池;`EnemyDeathSequence`
+做死亡前摇无敌演出并在演出中经动画事件生成小怪;`EnemyAnimationConfigSO.Dead` 提供动画)。
+**AI 图需要 `Death` 态只是为了让全局 `Died` 边有去处、并让决策停止。死亡不是一个决策。**
+
+**Files:**
+- Delete: `Assets/_Game/Scripts/Enemies/AIBrain/Modules/Death/`(整个目录,三个模块 + meta)
+- Delete: `Assets/_Game/Scripts/Enemies/AIBrain/Modules/IDeathModule.cs`(+ meta)
+- Delete: `Assets/Tests/EditMode/AI/DeathModuleTests.cs`(+ meta)
+
+- [ ] **Step 1: 删除三个死亡模块、契约与测试**
+
+```bash
+git rm -r Assets/_Game/Scripts/Enemies/AIBrain/Modules/Death Assets/_Game/Scripts/Enemies/AIBrain/Modules/Death.meta
+git rm Assets/_Game/Scripts/Enemies/AIBrain/Modules/IDeathModule.cs Assets/_Game/Scripts/Enemies/AIBrain/Modules/IDeathModule.cs.meta
+git rm Assets/Tests/EditMode/AI/DeathModuleTests.cs Assets/Tests/EditMode/AI/DeathModuleTests.cs.meta
+```
+
+- [ ] **Step 2: 确认无残留引用**
+
+```bash
+grep -rn "IDeathModule\|TerminalDeath\|AbilityDeath\|TwoStageDeath" Assets --include=*.cs
+```
+期望:无结果。(`PerceptionSkeleton` 尚未实现,所以此时不该有引用。)
+
+- [ ] **Step 3: 确认编译 + 全量测试**
+
+MCP 查编译,期望 0 错误。跑全量 EditMode:**从 247 回落到 243**(删掉 4 个死亡层测试)。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git commit -m "revert(enemy): 收掉死亡层——死亡归物理层,AI 只需一个终态"
+```
+
+死亡终态改由 `PerceptionSkeleton` 自己声明,见 Task 10。
+
---
## Task 10: `PerceptionSkeleton`
@@ -1651,8 +1709,7 @@ namespace BaseGames.Tests.EditMode.AI
var b = new BrainBuilder();
PerceptionSkeleton.Add(b,
unaware ?? new SinglePost(LocomotionMode.Patrol),
- engagement ?? new RushEngagement("rush", RushExit.OnLostTarget),
- new TerminalDeath());
+ engagement ?? new RushEngagement("rush", RushExit.OnLostTarget));
return new AiRuntime(b.Build(), ctx);
}
@@ -1767,7 +1824,7 @@ namespace BaseGames.Tests.EditMode.AI
Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName);
rt.Send(AiSignal.Died);
rt.Tick(0.1f);
- Assert.AreEqual(TerminalDeath.Death, rt.CurrentStateName);
+ Assert.AreEqual(PerceptionSkeleton.Death, rt.CurrentStateName);
}
[Test]
@@ -1778,7 +1835,7 @@ namespace BaseGames.Tests.EditMode.AI
ctx.V.Controllable = false; // 硬直中
rt.Send(AiSignal.Died);
rt.Tick(0.1f);
- Assert.AreEqual(TerminalDeath.Death, rt.CurrentStateName);
+ Assert.AreEqual(PerceptionSkeleton.Death, rt.CurrentStateName);
}
// ── 校验 ──────────────────────────────────────────────────────────
@@ -1806,7 +1863,7 @@ namespace BaseGames.Tests.EditMode.AI
{
var b = new BrainBuilder();
Assert.Throws(() => PerceptionSkeleton.Add(
- b, new BadUnaware(), new RushEngagement("rush"), new TerminalDeath()));
+ b, new BadUnaware(), new RushEngagement("rush")));
}
[Test]
@@ -1814,7 +1871,7 @@ namespace BaseGames.Tests.EditMode.AI
{
var b = new BrainBuilder();
Assert.Throws(() => PerceptionSkeleton.Add(
- b, new UndeclaredUnaware(), new RushEngagement("rush"), new TerminalDeath()));
+ b, new UndeclaredUnaware(), new RushEngagement("rush")));
}
[Test]
@@ -1822,7 +1879,7 @@ namespace BaseGames.Tests.EditMode.AI
{
var b = new BrainBuilder();
Assert.Throws(() => PerceptionSkeleton.Add(
- b, null, new RushEngagement("rush"), new TerminalDeath()));
+ b, null, new RushEngagement("rush")));
}
}
}
@@ -1844,8 +1901,8 @@ using BaseGames.AI;
namespace BaseGames.Enemies
{
///
- /// 感知骨架:把「未发现 → 警觉 → 交战」的升降级规则,挂到三个模块声明的态上。
- /// 骨架永远只做这一件事——未发现态长什么样、交战怎么打、怎么死,全部归模块。
+ /// 感知骨架:把「未发现 → 警觉 → 交战」的升降级规则,挂到两个模块声明的态上。
+ /// 骨架永远只做这一件事——未发现态长什么样、交战怎么打,全部归模块。
///
/// 规则:
/// 未发现:在追逐区 → 交战(优先);否则 有警觉态且在视野 → 警觉;否则保持。
@@ -1853,12 +1910,19 @@ namespace BaseGames.Enemies
/// 交战: 脱战条件由交战模块自定,脱战一律回 Rest —— 永不回警觉。
/// 死亡: 全局事件边,优先级最高,且不受 IsControllable 门阻挡。
///
+ /// 死亡态由骨架自己声明为无行为终态,不做成可插拔层——死亡在本项目里完全归物理层
+ /// (EnemyBase.PerformDeath 切终态/关碰撞/播死亡动画/归还池;EnemyDeathSequence 做
+ /// 死亡前摇演出并可经动画事件生成小怪)。且 PerformDeath 先 ForceState(Dead) 再发
+ /// Died 信号,此后 IsControllable 永久为假、AiRuntime 不再求值任何条件边,
+ /// 所以死亡链里放条件转换必然卡住。AI 图需要这个态只是为了让全局边有去处、决策停止。
+ ///
/// 入口默认取 unaware.Entry;需要前置态(掉落链 / 出场链)的敌人在调用本方法后
/// 再调 b.Entry("自己的态") 覆盖即可。
///
public static class PerceptionSkeleton
{
public const string Alert = "Alert";
+ public const string Death = "Death";
static readonly Func HasAlertAndInVision =
x => x is IEnemyActor a && a.HasAlertState && x.Sensor.InVisionZone();
@@ -1867,18 +1931,16 @@ namespace BaseGames.Enemies
public static void Add(BrainBuilder b,
IUnawareModule unaware,
- IEngagementModule engagement,
- IDeathModule death)
+ IEngagementModule engagement)
{
if (b == null) throw new ArgumentNullException(nameof(b));
if (unaware == null) throw new ArgumentNullException(nameof(unaware));
if (engagement == null) throw new ArgumentNullException(nameof(engagement));
- if (death == null) throw new ArgumentNullException(nameof(death));
// 1) 各模块先声明自己的态(未发现层此阶段只声明态,不挂内部边)
unaware.Declare(b);
engagement.Build(b, unaware.Rest);
- death.Build(b);
+ AiStateFragments.Terminal(b, Death);
var states = unaware.States;
if (states == null || states.Count == 0)
@@ -1890,7 +1952,7 @@ namespace BaseGames.Enemies
$"{unaware.GetType().Name}: Rest='{unaware.Rest}' 必须属于 States。");
b.Entry(unaware.Entry);
- b.Global().To(death.EntryState).OnEvent(AiSignal.Died);
+ b.Global().To(Death).OnEvent(AiSignal.Died);
// 2) 升级边先挂——AiRuntime 按声明顺序评估条件边,升级必须优先于
// 未发现层的内部边(如巡逻计时),否则玩家进追逐区那一帧可能被计时边抢走。
@@ -1949,7 +2011,7 @@ using BaseGames.AI;
namespace BaseGames.Enemies
{
///
- /// E001(草蛭)AI —— 纯决策层,组装三层模块。
+ /// E001(草蛭)AI —— 纯决策层,组装两层模块。
/// 出生伪装静止(伪装成石头),一旦交战过就只回巡逻;追击=带冷却的接触冲锋,
/// 起手即锁定、打完回巡逻走冷却;死亡演出走物理状态机(EnemyBase.PerformDeath)。
/// AI 只决策;移动 / 朝向 / 速度 / 动画 / 伤害全部在能力与 EnemyLocomotion 里实现。
@@ -1960,8 +2022,7 @@ namespace BaseGames.Enemies
{
protected override void Build(BrainBuilder b) => PerceptionSkeleton.Add(b,
unaware: new DisguiseThenPatrol(),
- engagement: new RushEngagement("e001_chase", RushExit.Committed),
- death: new TerminalDeath());
+ engagement: new RushEngagement("e001_chase", RushExit.Committed));
}
}
```
@@ -2130,8 +2191,7 @@ namespace BaseGames.Tests.EditMode.AI
var so = ScriptableObject.CreateInstance();
so.SetModulesForTests(
new DisguiseThenPatrol(),
- new RushEngagement("e001_chase", RushExit.Committed),
- new TerminalDeath());
+ new RushEngagement("e001_chase", RushExit.Committed));
return so;
}
@@ -2152,8 +2212,7 @@ namespace BaseGames.Tests.EditMode.AI
var b = new BrainBuilder();
PerceptionSkeleton.Add(b,
new DisguiseThenPatrol(),
- new RushEngagement("e001_chase", RushExit.Committed),
- new TerminalDeath());
+ new RushEngagement("e001_chase", RushExit.Committed));
var handWritten = b.Build();
Assert.AreEqual(handWritten.EntryState, fromRecipe.EntryState);
@@ -2235,16 +2294,17 @@ namespace BaseGames.Enemies
[Tooltip("交战层:发现玩家之后怎么打")]
[SerializeReference, SubclassSelector] IEngagementModule _engagement = new RushEngagement();
- [Tooltip("死亡层:怎么死")]
- [SerializeReference, SubclassSelector] IDeathModule _death = new TerminalDeath();
+ // 无「死亡层」字段:死亡归物理层,骨架自己声明 Death 终态。详见 spec 的
+ // 「修订:死亡层取消」——PerformDeath 先 ForceState(Dead) 再发 Died 信号,
+ // 此后 IsControllable 永久为假,死亡链里的条件边永不被求值。
protected override void Build(BrainBuilder b)
- => PerceptionSkeleton.Add(b, _unaware, _engagement, _death);
+ => PerceptionSkeleton.Add(b, _unaware, _engagement);
/// 仅供 EditMode 测试与脚手架向导装配模块,运行时不使用。
- public void SetModulesForTests(IUnawareModule unaware, IEngagementModule engagement, IDeathModule death)
+ public void SetModulesForTests(IUnawareModule unaware, IEngagementModule engagement)
{
- _unaware = unaware; _engagement = engagement; _death = death;
+ _unaware = unaware; _engagement = engagement;
}
}
}