diff --git a/Docs/superpowers/plans/2026-07-10-enemy-locomotion-refactor.md b/Docs/superpowers/plans/2026-07-10-enemy-locomotion-refactor.md
new file mode 100644
index 00000000..fcd7aa3d
--- /dev/null
+++ b/Docs/superpowers/plans/2026-07-10-enemy-locomotion-refactor.md
@@ -0,0 +1,786 @@
+# 敌人执行层统一 (EnemyLocomotion) Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 用统一的 `EnemyLocomotion` 执行器取代 7 种巡逻/4 套移动/`IMover`/三个协程能力,删除 `AiPhase` 影子层与 `EnemyStatsSO` 死字段,让敌人 AI 保持"只决策"、执行层单一化——先在 E001 落地并验证。
+
+**Architecture:** 三条正交轴:反应态 FSM(`EnemyStateType`) 当门,BrainGraph 决策(声明 locomotion 意图 + 触发攻击能力),执行层 = `EnemyLocomotion`(移动/朝向/步态动画) + `EnemyAbilityBase`(攻击)。`EnemyLocomotion` 是 `EnemyNavAgent`(地面 PB2d) / `FlyingDirectNavigator`(飞行直飞) 两个 `IPathAgent` 后端之上的唯一门面,对外暴露 `SetMode/Approach/MoveTo/Face/Stop`(声明意图,执行在内部)。
+
+**Tech Stack:** Unity 2022.3 (C# 9)、PathBerserker2d、Animancer、自研 BrainGraph、NUnit EditMode 测试、Unity MCP(端口 7890)做 PlayMode 验证。
+
+**前置状态:** P1(删死代码 BD_*/Opsive/宏) 已完成 (commit `e894a9d`),编译 0 错误。本计划从 P2 起。设计见 `Docs/superpowers/specs/2026-07-10-enemy-locomotion-refactor-design.md`。
+
+**全局约定:**
+- 每个 Task 结束提交一次;提交信息中文 + 类型前缀。
+- 编译验证统一用:`unity_execute_code` 触发 `AssetDatabase.Refresh()` + `RequestScriptCompilation()`,等待后 `unity_get_compilation_errors severity=error` 期望 count=0。下称"**编译门**"。
+- PlayMode 验证统一:`unity_play_mode play` → `unity_execute_code` 断言 → `unity_play_mode stop`。TestRoomA 为活动场景,E001 实例名含 "CaoZhi"。
+- 禁止在 AI 脚本(`AiScript`/`PerceptionStateMachine`)里出现 velocity/朝向/动画的**实现**;只允许声明意图。物理原生约束保持(寻路只给方向、velocity 执行、碰撞体底部 y=0)。
+
+---
+
+## File Structure
+
+**新建:**
+- `Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs` — 统一移动执行器 + `LocomotionMode`/`PatrolStrategy` 枚举 + `IEnemyLocomotion` 接口。
+- `Assets/Tests/EditMode/AI/Fakes/FakeLocomotion.cs` — 测试用 locomotion spy(记录调用)。
+
+**修改:**
+- `Assets/_Game/Scripts/AI/IAiContext.cs` — `IMover Mover` → `IEnemyLocomotion Locomotion`(注:接口在 Enemies 程序集,见 Task 说明)。
+- `Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs` — 去 `IMover` 实现,暴露 `Locomotion`。
+- `Assets/_Game/Scripts/Enemies/AIBrain/PerceptionStateMachine.cs` — `AddAbilityState` → `AddLocomotionState`(状态绑意图)。
+- `Assets/_Game/Scripts/Enemies/AIBrain/Ai/E001CaoZhiAi.cs` — 声明每状态 locomotion 意图 + Chase 能力。
+- `Assets/_Game/Scripts/Enemies/Abilities/ContactChaseAbility.cs` — 内部 `MoveTo` → `Locomotion.Approach`。
+- `Assets/_Game/Scripts/Enemies/EnemyBase.cs` — 缓存/暴露 `Locomotion`;删 `SetAiPhase`/`AiPhase` 相关(P4)。
+- `Assets/_Game/Scripts/Enemies/EnemyMovement.cs` — 删对 `EnemyStatsSO` 的速度直读(P5)。
+- `Assets/_Game/Scripts/Enemies/EnemyStatsSO.cs` — 删死字段(P5)。
+- `Assets/_Game/Scripts/Enemies/EnemyAnimationConfigSO.cs` / 动画驱动 — locomotion 模式→步态 clip(P4)。
+- `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs` — PlaceE001 挂 `EnemyLocomotion`、去三协程能力。
+- `Assets/Tests/EditMode/AI/AiRuntimeTests.cs`、`FakeAiContext.cs` — `Mover`→`Locomotion` spy。
+
+**删除:**
+- `Assets/_Game/Scripts/AI/IMover.cs`(+meta)
+- `Assets/_Game/Scripts/Enemies/Abilities/IdleAbility.cs`、`PatrolAbility.cs`、`AlertAbility.cs`(+meta)
+- `Assets/_Game/Data/Enemies/E001/Abilities/ABL_E001_Idle.asset`、`ABL_E001_Patrol.asset`(+ Alert 的处理见 Task)
+- `Assets/_Game/Scripts/Enemies/AiPhase.cs`(+meta)(P4)
+
+**接口归属说明:** `IEnemyLocomotion` 引用 `LocomotionMode`(Enemies 命名空间概念)。为避免 `BaseGames.AI` 程序集反向依赖 Enemies,采用与现有 `IEnemyActor` 相同的模式:`IAiContext.Locomotion` 的**类型**放在 `BaseGames.AI` 里定义为 `IEnemyLocomotion`(AI 程序集内),`LocomotionMode`/`PatrolStrategy` 枚举**也放 AI 程序集**(纯枚举无依赖),`EnemyLocomotion` MonoBehaviour 在 Enemies 程序集实现该接口。这与 `ISensor`/`ICombatant` 一致。
+
+---
+
+## P2 — 引入 EnemyLocomotion(与旧 API 并存,不改 AI)
+
+本阶段只新增组件并接到 E001,Model A 协程能力暂留、AI 不改。目的:`EnemyLocomotion` 能独立驱动 Idle/Patrol/Face/Approach。
+
+### Task 1: 定义 `IEnemyLocomotion` 接口与枚举(AI 程序集)
+
+**Files:**
+- Create: `Assets/_Game/Scripts/AI/IEnemyLocomotion.cs`
+
+- [ ] **Step 1: 创建接口文件**
+
+```csharp
+using UnityEngine;
+
+namespace BaseGames.AI
+{
+ /// 移动执行器的行为模式。
+ public enum LocomotionMode { Idle, Patrol, Face, Approach }
+
+ /// 巡逻策略(Patrol 模式下的具体走法)。
+ public enum PatrolStrategy { Wander, Pace, Waypoints }
+
+ ///
+ /// 敌人移动执行器的声明式接口。AI/能力只"声明意图",执行在实现内部完成
+ /// (AI 不 actuate)。实现见 BaseGames.Enemies.EnemyLocomotion。
+ ///
+ public interface IEnemyLocomotion
+ {
+ void SetMode(LocomotionMode mode); // Idle(停) / Patrol(按配置策略游走)
+ void Approach(Transform target); // 持续跟随,派生 RunSpeed
+ void MoveTo(Vector2 point); // 一次性目标点
+ void Face(Vector2 lookAt); // 停 + 朝向
+ void Stop();
+ LocomotionMode CurrentMode { get; }
+ bool IsMoving { get; }
+ }
+}
+```
+
+- [ ] **Step 2: 编译门** — 触发编译,`unity_get_compilation_errors` 期望 count=0。
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add Assets/_Game/Scripts/AI/IEnemyLocomotion.cs Assets/_Game/Scripts/AI/IEnemyLocomotion.cs.meta
+git commit -m "feat(ai): 新增 IEnemyLocomotion 接口与 LocomotionMode/PatrolStrategy 枚举"
+```
+
+### Task 2: 实现 `EnemyLocomotion` 组件(委托现有 EnemyBase/Nav)
+
+**Files:**
+- Create: `Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs`
+
+- [ ] **Step 1: 创建组件**(P2 只实现 Wander 巡逻;Pace/Waypoints 在 P6)
+
+```csharp
+using UnityEngine;
+using BaseGames.AI;
+
+namespace BaseGames.Enemies
+{
+ ///
+ /// 敌人移动执行器:唯一的移动/朝向入口。AI 状态与能力经 IEnemyLocomotion
+ /// 声明意图,本组件每帧把当前模式翻译为对 EnemyBase/IPathAgent 的调用。
+ /// 取代散落的 MoveTo/StopMovement/FacePlayer/FaceTarget 直调与三个协程能力。
+ ///
+ [DisallowMultipleComponent]
+ public sealed class EnemyLocomotion : MonoBehaviour, IEnemyLocomotion
+ {
+ [Header("巡逻策略(P6 实现 Pace/Waypoints;当前仅 Wander)")]
+ [SerializeField] private PatrolStrategy _patrolStrategy = PatrolStrategy.Wander;
+
+ private EnemyBase _enemy;
+ private LocomotionMode _mode = LocomotionMode.Idle;
+ private Transform _approachTarget;
+ private Vector2 _facePoint;
+
+ public LocomotionMode CurrentMode => _mode;
+ public bool IsMoving => _enemy != null && _enemy.Nav != null && _enemy.Nav.IsMoving;
+
+ private void Awake()
+ {
+ _enemy = GetComponentInParent();
+ if (_enemy == null)
+ Debug.LogError("[EnemyLocomotion] 找不到 EnemyBase。", this);
+ }
+
+ // ── IEnemyLocomotion(声明意图,不立即 actuate 之外的副作用)──
+ public void SetMode(LocomotionMode mode)
+ {
+ if (_mode == mode) return;
+ _mode = mode;
+ if (mode == LocomotionMode.Idle) _enemy?.StopMovement();
+ if (mode == LocomotionMode.Patrol && _enemy?.Stats != null)
+ _enemy.Nav?.SetSpeed(_enemy.Stats.WalkSpeed);
+ }
+
+ public void Approach(Transform target)
+ {
+ _mode = LocomotionMode.Approach;
+ _approachTarget = target;
+ if (_enemy?.Stats != null) _enemy.Nav?.SetSpeed(_enemy.Stats.RunSpeed);
+ }
+
+ public void MoveTo(Vector2 point)
+ {
+ _mode = LocomotionMode.Approach;
+ _approachTarget = null;
+ _enemy?.MoveTo(point);
+ }
+
+ public void Face(Vector2 lookAt)
+ {
+ _mode = LocomotionMode.Face;
+ _facePoint = lookAt;
+ }
+
+ public void Stop()
+ {
+ _mode = LocomotionMode.Idle;
+ _enemy?.StopMovement();
+ }
+
+ // ── 每帧把模式翻译成执行 ──
+ private void Update()
+ {
+ if (_enemy == null) return;
+ switch (_mode)
+ {
+ case LocomotionMode.Idle:
+ break; // SetMode(Idle) 已停;保持
+ case LocomotionMode.Patrol:
+ TickPatrol();
+ break;
+ case LocomotionMode.Face:
+ _enemy.StopMovement();
+ _enemy.FaceTarget(_facePoint);
+ break;
+ case LocomotionMode.Approach:
+ if (_approachTarget != null) _enemy.MoveTo(_approachTarget.position);
+ break;
+ }
+ }
+
+ private void TickPatrol()
+ {
+ var nav = _enemy.Nav;
+ if (nav == null) return;
+ switch (_patrolStrategy)
+ {
+ case PatrolStrategy.Wander:
+ if (!nav.IsMoving) nav.WalkToRandom();
+ break;
+ // Pace / Waypoints:P6 实现
+ default:
+ if (!nav.IsMoving) nav.WalkToRandom();
+ break;
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 2: 编译门** — count=0。
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs.meta
+git commit -m "feat(enemy): 新增 EnemyLocomotion 执行器(Idle/Patrol-Wander/Face/Approach)"
+```
+
+### Task 3: 缓存并暴露 `EnemyBase.Locomotion`
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Enemies/EnemyBase.cs`(字段区 + Awake 缓存 + 属性)
+
+- [ ] **Step 1: 加字段与属性**(放在 `_movement`/`_nav` 等子系统字段旁;照抄现有序列化字段风格)
+
+在 EnemyBase 的子系统引用区新增:
+```csharp
+ [SerializeField] private EnemyLocomotion _locomotion;
+ public EnemyLocomotion Locomotion => _locomotion;
+```
+
+- [ ] **Step 2: Awake 兜底获取**(在现有 `_stats.Initialize` 附近,若未序列化则 GetComponent)
+
+```csharp
+ if (_locomotion == null) _locomotion = GetComponentInChildren();
+```
+
+- [ ] **Step 3: 编译门** — count=0。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Enemies/EnemyBase.cs
+git commit -m "feat(enemy): EnemyBase 缓存并暴露 Locomotion"
+```
+
+### Task 4: 脚手架 PlaceE001 挂载 EnemyLocomotion 并绑定引用;重生成 prefab
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs`(PlaceE001_CaoZhi 内)
+
+- [ ] **Step 1: 在 PlaceE001 里挂组件并绑 EnemyBase._locomotion**(`using BaseGames.Enemies;` 已在文件顶部)
+
+在 `var brain = GetOrAddComponent(go);` 之前插入:
+```csharp
+ var locomotion = GetOrAddComponent(go);
+ AssignReference(enemyBase, "_locomotion", locomotion, report);
+```
+
+- [ ] **Step 2: 编译门** — count=0。
+
+- [ ] **Step 3: 重生成 E001 prefab(脚手架为权威)**
+
+用 `unity_execute_code` 反射调用 `PlaceAndSaveEnemyPrefab("ENM_CaoZhi", PlaceE001_CaoZhi, removeSceneInstance:true)`(同既往做法)。注意此调用较慢、桥接可能短时超时,超时后 `unity_editor_ping` 等恢复再继续。
+
+- [ ] **Step 4: 用 prefab 实例替换 TestRoomA 场景实例并保存**(保留位置;`PrefabUtility.InstantiatePrefab`,`EditorSceneManager.SaveScene`)。
+
+- [ ] **Step 5: 验证 prefab 挂了 EnemyLocomotion 且 EnemyBase._locomotion 已绑**
+
+`unity_execute_code`:加载 prefab,断言 `GetComponentInChildren() != null` 且 EnemyBase 序列化字段 `_locomotion` 非空。期望都为真。
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs Assets/_Game/Prefabs/Enemies/E001/ENM_CaoZhi.prefab Assets/_Game/Scenes/Testings/TestRoomA.unity
+git commit -m "feat(scaffold): PlaceE001 挂载 EnemyLocomotion 并绑定引用,重生成 prefab"
+```
+
+### Task 5: PlayMode 冒烟验证 EnemyLocomotion 独立可用
+
+**Files:** 无(仅 MCP 验证)
+
+- [ ] **Step 1: 进入 PlayMode**,`unity_play_mode play`,等待 ~3s。
+
+- [ ] **Step 2: 直接驱动 locomotion(绕过 AI)并断言**
+
+`unity_execute_code`:
+```csharp
+var e = UnityEngine.Object.FindObjectsOfType(true).FirstOrDefault(x=>x.name.Contains("CaoZhi"));
+var loco = e.Locomotion;
+loco.Approach(e.PlayerTransform); // 让它朝玩家移动
+return "mode=" + loco.CurrentMode; // 期望 Approach
+```
+等待 ~2s 后再断言 `loco.IsMoving == true`(或位置发生变化)。再 `loco.SetMode(BaseGames.AI.LocomotionMode.Idle)`,断言随后 `IsMoving == false`。
+
+- [ ] **Step 3: console 0 报错**,`unity_console_log type=error` count=0。
+
+- [ ] **Step 4: 退出 PlayMode**,`unity_play_mode stop`。(无代码改动,无需提交。)
+
+---
+
+## P3 — AI 改绑 locomotion 意图,删协程能力/IMover
+
+### Task 6: 编写失败测试 — PerceptionStateMachine 状态设置正确 locomotion 意图
+
+**Files:**
+- Create: `Assets/Tests/EditMode/AI/Fakes/FakeLocomotion.cs`
+- Modify: `Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs`
+- Create/Modify: `Assets/Tests/EditMode/AI/PerceptionStateMachineTests.cs`
+
+- [ ] **Step 1: 创建 FakeLocomotion spy**
+
+```csharp
+using System.Collections.Generic;
+using UnityEngine;
+using BaseGames.AI;
+
+namespace BaseGames.Tests.EditMode.AI
+{
+ public sealed class FakeLocomotion : IEnemyLocomotion
+ {
+ public List Calls = new List();
+ public LocomotionMode CurrentMode { get; private set; }
+ public bool IsMoving { get; set; }
+ public void SetMode(LocomotionMode mode) { CurrentMode = mode; Calls.Add("SetMode:" + mode); }
+ public void Approach(Transform t) { CurrentMode = LocomotionMode.Approach; Calls.Add("Approach"); }
+ public void MoveTo(Vector2 p) { Calls.Add("MoveTo"); }
+ public void Face(Vector2 p) { CurrentMode = LocomotionMode.Face; Calls.Add("Face"); }
+ public void Stop() { CurrentMode = LocomotionMode.Idle; Calls.Add("Stop"); }
+ }
+}
+```
+
+- [ ] **Step 2: 在 FakeAiContext 用 Locomotion 替换 Mover**
+
+将 `FakeAiContext` 中 `public FakeMover M ...` / `public IMover Mover => M;` 替换为:
+```csharp
+ public FakeLocomotion L = new FakeLocomotion();
+ public IEnemyLocomotion Locomotion => L;
+```
+(`FakeMover` 类可整体删除。)
+
+- [ ] **Step 3: 写测试(先失败)**——用一个最小 `AiScript` 经 `PerceptionStateMachine` 构图,驱动到各态断言意图。因 `PerceptionStateMachine` 目前还是 `AddAbilityState`,此测试会编译失败或断言失败。
+
+```csharp
+using NUnit.Framework;
+using BaseGames.AI;
+using BaseGames.Enemies;
+
+namespace BaseGames.Tests.EditMode.AI
+{
+ public class PerceptionStateMachineTests
+ {
+ static AiGraph Graph()
+ {
+ var b = new BrainBuilder();
+ PerceptionStateMachine.Add(b, new PerceptionStateMachine.Config
+ {
+ Idle = "Idle", Patrol = "Patrol", Alert = "Alert", Chase = "Chase",
+ Death = "Death", Entry = "Idle", Rest = "Patrol",
+ IdleMode = LocomotionMode.Idle,
+ PatrolMode = LocomotionMode.Patrol,
+ AlertMode = LocomotionMode.Face,
+ ChaseAbilityId = "chase",
+ });
+ return b.Build();
+ }
+
+ [Test]
+ public void IdleState_SetsIdleMode()
+ {
+ var ctx = new FakeAiContext();
+ var rt = new AiRuntime(Graph(), ctx);
+ Assert.AreEqual(LocomotionMode.Idle, ctx.L.CurrentMode);
+ }
+
+ [Test]
+ public void ChaseState_TriggersChaseAbility_NotLocomotionDirectly()
+ {
+ var ctx = new FakeAiContext();
+ var rt = new AiRuntime(Graph(), ctx);
+ ctx.S.InChaseZoneValue = true; // 见下方 FakeSensor 字段
+ rt.Tick(0.1f);
+ Assert.AreEqual("Chase", rt.CurrentStateName);
+ CollectionAssert.Contains(ctx.C.Calls, "UseAbility:chase");
+ }
+ }
+}
+```
+(若 `FakeSensor`/`FakeCombat` 缺 `InChaseZoneValue`/`Calls` 字段,在其 Fake 中补上最小实现——见 Step 记录。)
+
+- [ ] **Step 4: 运行 EditMode 测试,确认失败**(Unity Test Runner → EditMode,或 MCP 运行测试)。期望:编译失败(`Config.IdleMode` 不存在)或断言失败。这是预期红。
+
+- [ ] **Step 5: 提交测试(红)**
+
+```bash
+git add Assets/Tests/EditMode/AI/Fakes/FakeLocomotion.cs Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs Assets/Tests/EditMode/AI/PerceptionStateMachineTests.cs
+git commit -m "test(ai): PerceptionStateMachine locomotion 意图测试(先失败)"
+```
+
+### Task 7: 切换 IAiContext.Mover → Locomotion,删 IMover
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/AI/IAiContext.cs`
+- Delete: `Assets/_Game/Scripts/AI/IMover.cs`(+meta)
+- Modify: `Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs`
+- Modify: `Assets/Tests/EditMode/AI/AiRuntimeTests.cs`
+
+- [ ] **Step 1: IAiContext 换成员**
+
+在 `IAiContext.cs` 把 `IMover Mover { get; }` 改为 `IEnemyLocomotion Locomotion { get; }`。
+
+- [ ] **Step 2: 删 IMover.cs**
+
+```bash
+rm Assets/_Game/Scripts/AI/IMover.cs Assets/_Game/Scripts/AI/IMover.cs.meta
+```
+
+- [ ] **Step 3: EnemyBrainContext 实现 Locomotion,删 IMover 实现块**
+
+- 类声明去掉 `IMover`。
+- 删除 `// ---- IMover ----` 整段(`MoveTo/FacePlayer/Stop/WalkRandom/LookAround/UseChaseSpeed/UsePatrolSpeed`)。
+- 把 `public IMover Mover => this;` 改为 `public IEnemyLocomotion Locomotion => _enemy.Locomotion;`。
+
+- [ ] **Step 4: 修 AiRuntimeTests 里的 `c.Mover.*` 引用**——那些测试用 Mover 当通用副作用 spy,改用 Locomotion spy 语义:
+- `.Tick(c => c.Mover.WalkRandom())` → `.Tick(c => c.Locomotion.SetMode(LocomotionMode.Patrol))`
+- `.OnEnter(c => c.Mover.FacePlayer())` → `.OnEnter(c => c.Locomotion.Face(c.Sensor.LastKnown))`
+- `.OnExit(c => c.Mover.Stop())` → `.OnExit(c => c.Locomotion.Stop())`
+- `.Tick(c => c.Mover.MoveTo(c.Sensor.LastKnown))` → `.Tick(c => c.Locomotion.MoveTo(c.Sensor.LastKnown))`
+- `.OnEnter(c => c.Mover.LookAround())` → `.OnEnter(c => c.Locomotion.SetMode(LocomotionMode.Idle))`
+- 断言里 `ctx.M.Calls` → `ctx.L.Calls`,字符串相应改(如 `"WalkRandom"` → `"SetMode:Patrol"`、`"FacePlayer"`→`"Face"`、`"Stop"`→`"Stop"`、`"LookAround"`→`"SetMode:Idle"`)。
+
+- [ ] **Step 5: 编译门** — count=0。
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add -A Assets/_Game/Scripts/AI Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs Assets/Tests/EditMode/AI/AiRuntimeTests.cs
+git commit -m "refactor(ai): IAiContext.Mover→Locomotion,删除 IMover"
+```
+
+### Task 8: PerceptionStateMachine 改 AddLocomotionState(让 Task 6 测试转绿)
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Enemies/AIBrain/PerceptionStateMachine.cs`
+
+- [ ] **Step 1: 重写 Config 与状态构建**
+
+`Config` 把 `IdleAbilityId/PatrolAbilityId/AlertAbilityId` 换成 locomotion 意图字段,保留 `ChaseAbilityId`:
+```csharp
+ public sealed class Config
+ {
+ public string Idle = "Idle", Patrol = "Patrol", Alert = "Alert",
+ Chase = "Chase", Death = "Death", Entry = "Idle", Rest = "Patrol";
+ public LocomotionMode IdleMode = LocomotionMode.Idle;
+ public LocomotionMode PatrolMode = LocomotionMode.Patrol;
+ public LocomotionMode AlertMode = LocomotionMode.Face; // 停+朝向玩家
+ public string ChaseAbilityId; // Chase 走能力
+ public string DeathAbilityId; // 通常留空
+ }
+```
+
+状态构建:Idle/Patrol/Alert 用 locomotion 意图;Chase 用能力;转换规则不变(照抄现有 `.To(...).When(...)`)。新增私有助手:
+```csharp
+ static BrainBuilder.StateBuilder AddLocomotionState(BrainBuilder b, string state, LocomotionMode mode, bool facePlayer)
+ {
+ return b.State(state)
+ .OnEnter(x => Apply(x, mode, facePlayer))
+ .Tick(x => Apply(x, mode, facePlayer))
+ .OnExit(x => x.Locomotion.Stop());
+ }
+ static void Apply(IAiContext x, LocomotionMode mode, bool facePlayer)
+ {
+ if (facePlayer && x is IEnemyActor && x.Sensor != null)
+ { /* Face 需玩家点,见下 */ }
+ switch (mode)
+ {
+ case LocomotionMode.Face: x.Locomotion.Face(x.Sensor.LastKnown); break;
+ default: x.Locomotion.SetMode(mode); break;
+ }
+ }
+ static BrainBuilder.StateBuilder AddAbilityState(BrainBuilder b, string state, string abilityId)
+ {
+ return b.State(state)
+ .OnEnter(x => EnsureAbility(x, abilityId))
+ .Tick(x => EnsureAbility(x, abilityId))
+ .OnExit(x => x.Combat.InterruptAbilities());
+ }
+```
+说明:`Face` 用 `Sensor.LastKnown`(`EnemyBrainContext.Refresh` 每帧更新为玩家位置),避免 AI 直接引用玩家 Transform;`AddAbilityState` 仅 Chase/Death 用。`EnsureAbility`/`HasAlert` 保持原实现。
+
+`Add(b,c)`:Idle/Patrol 用 `AddLocomotionState(b, c.Idle, c.IdleMode, false)` / `(c.Patrol, c.PatrolMode, false)`,Alert 用 `AddLocomotionState(b, c.Alert, c.AlertMode, true)`,Chase 用 `AddAbilityState(b, c.Chase, c.ChaseAbilityId)`,Death 用 `AddAbilityState(b, c.Death, c.DeathAbilityId)`。转换 `.To().When()` 全部照抄现有。
+
+- [ ] **Step 2: 运行 Task 6 的 EditMode 测试,转绿**。期望 `IdleState_SetsIdleMode` 与 `ChaseState_TriggersChaseAbility` PASS。
+
+- [ ] **Step 3: 编译门** — count=0。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Enemies/AIBrain/PerceptionStateMachine.cs
+git commit -m "refactor(ai): PerceptionStateMachine 状态改绑 locomotion 意图,测试转绿"
+```
+
+### Task 9: E001CaoZhiAi 声明意图;ContactChaseAbility 用 Approach;删三协程能力/SO
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Enemies/AIBrain/Ai/E001CaoZhiAi.cs`
+- Modify: `Assets/_Game/Scripts/Enemies/Abilities/ContactChaseAbility.cs`
+- Delete: `IdleAbility.cs`、`PatrolAbility.cs`、`AlertAbility.cs`(+meta)、`ABL_E001_Idle.asset`、`ABL_E001_Patrol.asset`(+meta)
+
+- [ ] **Step 1: E001CaoZhiAi 用意图 Config**
+
+```csharp
+ protected override void Build(BrainBuilder b)
+ {
+ PerceptionStateMachine.Add(b, new PerceptionStateMachine.Config
+ {
+ Idle = "Idle_Disguise", Patrol = "Move_Patrol", Alert = "Alert",
+ Chase = "Chase", Death = "Death", Entry = "Idle_Disguise", Rest = "Move_Patrol",
+ IdleMode = LocomotionMode.Idle,
+ PatrolMode = LocomotionMode.Patrol,
+ AlertMode = LocomotionMode.Face,
+ ChaseAbilityId = "e001_chase",
+ });
+ }
+```
+(加 `using BaseGames.AI;` 若缺。)
+
+- [ ] **Step 2: ContactChaseAbility 内部改 Approach**
+
+把 `ExecuteCoroutine` 里的追击循环由 `_enemy.MoveTo(_enemy.PlayerTransform.position)` 改为经 locomotion:
+- 起始:`if (_enemy.Nav != null && _enemy.Stats != null) _enemy.Nav.SetSpeed(_enemy.Stats.RunSpeed);` 保留即可(Approach 内也会设),并改追击循环体为:
+```csharp
+ _enemy.Locomotion.Approach(_enemy.PlayerTransform);
+ while (_enemy.PlayerTransform != null)
+ yield return null;
+```
+(`Approach` 每帧由 EnemyLocomotion.Update 维持朝玩家移动,能力不再逐帧 MoveTo。)`CleanupChase()` 保持(关接触伤害);`OnInterrupted` 里追加 `_enemy.Locomotion.Stop();`。删除 `SetAiPhase(AiPhase.Chase)` 行(P4 会统一删,但这里先删避免编译依赖 AiPhase;若 P4 未做,暂留由 P4 清)。
+> 注:`SetAiPhase` 的删除统一在 P4;本 Task 若 AiPhase 尚存则保留该行,Approach 改造与之无关。
+
+- [ ] **Step 3: 删三协程能力与两个 SO**
+
+```bash
+rm Assets/_Game/Scripts/Enemies/Abilities/IdleAbility.cs Assets/_Game/Scripts/Enemies/Abilities/IdleAbility.cs.meta
+rm Assets/_Game/Scripts/Enemies/Abilities/PatrolAbility.cs Assets/_Game/Scripts/Enemies/Abilities/PatrolAbility.cs.meta
+rm Assets/_Game/Scripts/Enemies/Abilities/AlertAbility.cs Assets/_Game/Scripts/Enemies/Abilities/AlertAbility.cs.meta
+rm Assets/_Game/Data/Enemies/E001/Abilities/ABL_E001_Idle.asset Assets/_Game/Data/Enemies/E001/Abilities/ABL_E001_Idle.asset.meta
+rm Assets/_Game/Data/Enemies/E001/Abilities/ABL_E001_Patrol.asset Assets/_Game/Data/Enemies/E001/Abilities/ABL_E001_Patrol.asset.meta
+```
+(`ABL_E001_Alert.asset` 是否删见 Task 10:Alert 不再是能力,其 SO 也应删;此处一并 `rm` 它 +meta。)
+
+- [ ] **Step 4: 编译门** — count=0(若报 AiPhase 相关错,说明 Step 2 注保留项处理不当,回退保留 `SetAiPhase` 行)。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add -A Assets/_Game/Scripts/Enemies/AIBrain/Ai/E001CaoZhiAi.cs Assets/_Game/Scripts/Enemies/Abilities Assets/_Game/Data/Enemies/E001/Abilities
+git commit -m "refactor(ai): E001 状态改声明 locomotion 意图;Chase 走 Approach;删三协程能力/SO"
+```
+
+### Task 10: 脚手架 PlaceE001 去三能力子节点;重生成 prefab;替换场景实例
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs`(PlaceE001)
+
+- [ ] **Step 1: 删掉 IdleAbility_Idle/PatrolAbility_Patrol/AlertAbility_Alert 三节点创建与其 AssignAsset**——回到只有 `ContactChaseAbility_Chase`(保留其 `_config=ABL_E001_Chase` 与 `_contactDamage` 绑定)。删除对应的 `GetOrAddComponent/PatrolAbility/AlertAbility` 与 `AssignAsset(...ABL_E001_Idle/Patrol/Alert...)` 行。
+
+- [ ] **Step 2: 编译门** — count=0。
+
+- [ ] **Step 3: 重生成 prefab**(反射调 `PlaceAndSaveEnemyPrefab("ENM_CaoZhi", PlaceE001_CaoZhi, removeSceneInstance:true)`,超时则等桥接恢复)。
+
+- [ ] **Step 4: 替换 TestRoomA 场景实例并保存**(同 Task 4 Step 4)。
+
+- [ ] **Step 5: 验证 prefab 只剩 ContactChaseAbility,且挂了 EnemyLocomotion**(`unity_execute_code` 断言 `GetComponentsInChildren()` 只含 `ContactChaseAbility`;`EnemyLocomotion` 存在)。
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs Assets/_Game/Prefabs/Enemies/E001/ENM_CaoZhi.prefab Assets/_Game/Scenes/Testings/TestRoomA.unity
+git commit -m "refactor(scaffold): PlaceE001 去三协程能力子节点,重生成 prefab"
+```
+
+### Task 11: PlayMode 验证 E001 四态经 locomotion 驱动
+
+**Files:** 无
+
+- [ ] **Step 1: PlayMode play,等 ~3s。**
+
+- [ ] **Step 2: 玩家在近处 → 断言 Chase**:`state=Chase`、`Locomotion.CurrentMode=Approach`、ContactChaseAbility running=true、接触伤害在追击时开启。
+
+- [ ] **Step 3: 把敌人移到远处(80,5),等 ~3s → 断言 de-escalation**:`state=Move_Patrol`、`Locomotion.CurrentMode=Patrol`、敌人在游走(位置变化 / IsMoving)。
+
+- [ ] **Step 4: 断言 AI 脚本零 actuation**:确认 `E001CaoZhiAi`/`PerceptionStateMachine` 源码不含 velocity/朝向实现(人工核对 + grep:`rg -n "velocity|MovePosition|transform\.position\s*=" Assets/_Game/Scripts/Enemies/AIBrain` 期望 0 命中)。
+
+- [ ] **Step 5: console 0 报错;退出 PlayMode。**(无代码改动,无需提交。)
+
+---
+
+## P4 — 删 AiPhase,动画由 locomotion 驱动
+
+### Task 12: EnemyLocomotion 驱动步态动画
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs`
+- 参考: `Assets/_Game/Scripts/Enemies/EnemyAnimationConfigSO.cs`(`Idle/Walk/Run/Alert` clip 字段)
+
+- [ ] **Step 1: 在 EnemyLocomotion 缓存 Animancer + AnimConfig,按模式播步态**
+
+Awake 里取 `_enemy.Animancer` 与 AnimConfig(经 EnemyBase 暴露的 `_animConfig` 访问器;若无访问器,在 EnemyBase 加 `public EnemyAnimationConfigSO AnimConfig => _animConfig;`)。在模式切换时播对应 clip:
+```csharp
+ void PlayGait(LocomotionMode mode)
+ {
+ if (_animancer == null || _animConfig == null) return;
+ var clip = mode switch
+ {
+ LocomotionMode.Idle => _animConfig.Idle,
+ LocomotionMode.Patrol => _animConfig.Walk,
+ LocomotionMode.Face => _animConfig.Alert,
+ LocomotionMode.Approach => _animConfig.Run,
+ _ => null,
+ };
+ if (clip != null) _animancer.Play(clip);
+ }
+```
+在 `SetMode/Approach/Face/Stop` 模式变化处调用 `PlayGait(_mode)`(仅在模式真正变化时,避免每帧重播)。
+
+- [ ] **Step 2: 编译门** — count=0。
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs Assets/_Game/Scripts/Enemies/EnemyBase.cs
+git commit -m "feat(enemy): EnemyLocomotion 按模式驱动步态动画(Idle/Walk/Alert/Run)"
+```
+
+### Task 13: 删除 AiPhase 枚举与 SetAiPhase,迁移调用点
+
+**Files:**
+- Delete: `Assets/_Game/Scripts/Enemies/AiPhase.cs`(+meta)
+- Modify: `EnemyBase.cs`(删 `SetAiPhase`/`_currentAiPhase`/`CurrentAiPhase`/`OnAiPhaseChanged`/`_autoPlayPhaseAnimation`/phase→clip switch)
+- Modify: 所有能力里的 `SetAiPhase(...)` 调用(`ContactChaseAbility`、`AppearAbility`、`AnimatedCeilingDropAbility` 等)
+- Modify: `ReceiveAlert` 的判据;gizmo/`EnemyDebugOverlay` 读 AiPhase 处
+
+- [ ] **Step 1: 先移除所有 `SetAiPhase(...)` 调用**——grep 定位:`rg -n "SetAiPhase|CurrentAiPhase|AiPhase\.|OnAiPhaseChanged" Assets/_Game/Scripts`。逐处删除或替换:
+ - 能力里 `_enemy.SetAiPhase(AiPhase.X)` 直接删(动画已由 locomotion/能力自身 clip 驱动)。
+ - `ReceiveAlert` 中 `if (CurrentAiPhase == Chase || Combat) return;` 的"已交战不降级"判据,改为读 AI 状态:新增 `EnemyBase.IsEngaged`(由 `EnemyAiBrain` 暴露当前状态名是否为 Chase/攻击态)或简单 `bool _engaged` 由 Chase 能力 Execute/结束时置位。选后者:`ContactChaseAbility` 起始 `_enemy.SetEngaged(true)`,`CleanupChase` 里 `_enemy.SetEngaged(false)`;`EnemyBase` 加 `bool IsEngaged` + `SetEngaged`。
+ - gizmo/overlay 读 phase 处改读 `EnemyAiBrain` 当前状态名(`Brain` 已暴露)或删可视化。
+
+- [ ] **Step 2: 删 EnemyBase 内 AiPhase 成员与 phase→clip switch**(`SetAiPhase` 方法体、字段、事件、`_autoPlayPhaseAnimation`)。
+
+- [ ] **Step 3: 删 AiPhase.cs**
+
+```bash
+rm Assets/_Game/Scripts/Enemies/AiPhase.cs Assets/_Game/Scripts/Enemies/AiPhase.cs.meta
+```
+
+- [ ] **Step 4: 编译门** — count=0(把所有残留 `AiPhase` 引用清干净)。
+
+- [ ] **Step 5: PlayMode 回归**:E001 四态动画正确(待机=Idle、巡逻=Walk、警觉=Alert、追击=Run),受击/死亡 clip 能压过步态;console 0 报错。
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add -A Assets/_Game/Scripts/Enemies
+git commit -m "refactor(enemy): 删除 AiPhase 影子层,动画统一由 locomotion/反应态/能力驱动"
+```
+
+---
+
+## P5 — EnemyStatsSO 死字段清理 + 速度单一来源
+
+### Task 14: 删死字段,速度经 Locomotion 单一来源
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Enemies/EnemyStatsSO.cs`
+- Modify: `Assets/_Game/Scripts/Enemies/EnemyMovement.cs`(删 `_config` 速度直读)
+- Modify: `Assets/_Game/Scripts/Enemies/EnemyStats.cs`(删对应 pass-through 属性)
+
+- [ ] **Step 1: 先 grep 确认无运行期读取**——对每个待删字段跑:`rg -n "AttackDamage|AttackRange|DetectRange|DetectAngleDeg|EyeOffset|LOSBlockingMask|AlertDuration|InvestigateDuration|KnockbackForce|HitStunDuration|heavyHitThreshold" Assets/_Game/Scripts`。确认仅 Editor bestiary/gizmo 引用(这些一并处理或保留 gizmo 用的最小项)。
+
+- [ ] **Step 2: 删字段**——从 `EnemyStatsSO.cs` 删除:`AttackDamage`、`AttackRange`、`DetectRange`、`DetectAngleDeg`、`EyeOffset`、`LOSBlockingMask`、`AlertDuration`、`InvestigateDuration`、`KnockbackForce`、`HitStunDuration`、`HitTierConfig.heavyHitThreshold`。同步删 `EnemyStats.cs` 对应 pass-through 属性(若有 reader 则改指向留存字段)。Editor 里引用被删字段处(`EnemyModule.cs` bestiary 显示)改为不显示或显示 `DamageSourceSO`/sensor 槽的权威值。
+
+- [ ] **Step 3: EnemyMovement 速度单一来源**——删 `EnemyMovement` 内对 `EnemyStatsSO _config` 的速度直读(`_config.WalkSpeed`/`RunSpeed` 处),改由 `EnemyLocomotion`/`Nav.SetSpeed` 提供的 `PendingInput.MoveSpeed` 驱动;`_config` 若仅用于速度则整个移除该序列化引用。
+
+- [ ] **Step 4: 编译门** — count=0。
+
+- [ ] **Step 5: PlayMode 回归**:E001 巡逻速度=WalkSpeed、追击速度=RunSpeed,行为不变;console 0 报错。
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add -A Assets/_Game/Scripts/Enemies
+git commit -m "refactor(enemy): 删 EnemyStatsSO ~11 个死字段,速度单一来源"
+```
+
+---
+
+## P6 — 巡逻策略补全(Pace / Waypoints)
+
+### Task 15: 实现 Pace(撞墙/悬崖翻向踱步)
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs`
+
+- [ ] **Step 1: 加 Pace 参数与逻辑**——参照已删 `BD_Patrol` 的踱步语义(`MoveInDirection(_dir)` 每帧,遇 `IsWallAhead || IsLedgeAhead` 翻向)。EnemyLocomotion 加 `int _paceDir = 1;` 与序列化的墙/崖检测引用(复用 `EnemyMovement` 的 `IsWallAhead`/`IsLedgeAhead` 若存在;否则用 `WallDetector`):
+```csharp
+ case PatrolStrategy.Pace:
+ if (_enemy.Movement != null &&
+ (_enemy.Movement.IsWallAhead || _enemy.Movement.IsLedgeAhead))
+ _paceDir = -_paceDir;
+ _enemy.MoveInDirection(_paceDir);
+ break;
+```
+(若 `EnemyBase.Movement`/`IsWallAhead`/`IsLedgeAhead`/`MoveInDirection` 命名不同,按实际签名调整——实现前先 grep 确认。)
+
+- [ ] **Step 2: 编译门** — count=0。
+
+- [ ] **Step 3: EditMode/PlayMode 验证**——把 E001(或一测试敌人)`_patrolStrategy=Pace`,PlayMode 观察其在平台上来回踱步、遇墙/崖翻向。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs
+git commit -m "feat(enemy): EnemyLocomotion 巡逻策略 Pace(撞墙/悬崖翻向踱步)"
+```
+
+### Task 16: 实现 Waypoints(路点序列巡逻)
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs`
+
+- [ ] **Step 1: 加 Waypoints 参数与逻辑**——参照已删 `BD_PatrolWaypoints`(有序 `Transform[]`/`Vector2[]`,loop 或 ping-pong,`MoveTo(waypoint)` 到达推进 index):
+```csharp
+ [Header("Waypoints 策略")]
+ [SerializeField] private Transform[] _waypoints;
+ [SerializeField] private bool _pingPong;
+ private int _wpIndex; private int _wpDir = 1;
+ // TickPatrol 的 Waypoints 分支:
+ case PatrolStrategy.Waypoints:
+ if (_waypoints != null && _waypoints.Length > 0)
+ {
+ var nav = _enemy.Nav;
+ if (nav != null && !nav.IsMoving)
+ {
+ AdvanceWaypoint();
+ _enemy.MoveTo(_waypoints[_wpIndex].position);
+ }
+ }
+ break;
+```
+`AdvanceWaypoint()`:loop 时 `_wpIndex = (_wpIndex+1) % len`;ping-pong 时到端点翻 `_wpDir`。
+
+- [ ] **Step 2: 编译门** — count=0。
+
+- [ ] **Step 3: PlayMode 验证**——放置带 2-3 个路点的测试敌人,观察按序巡逻 + loop/ping-pong。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs
+git commit -m "feat(enemy): EnemyLocomotion 巡逻策略 Waypoints(路点序列 loop/ping-pong)"
+```
+
+---
+
+## 收尾
+
+### Task 17: 全量自检 + 更新记忆
+
+- [ ] **Step 1: 运行项目自检**(CLAUDE.md §3):`Validate All ScriptableObjects`、`Validate Address Keys`、`Physics2D Layer Matrix Check`——期望全绿。
+- [ ] **Step 2: 最终 PlayMode 全链回归**:E001 待机(伪装)→巡逻→警觉→追击(接触伤害)→死亡 + de-escalation,console 0 报错。
+- [ ] **Step 3: grep 确认清理彻底**:`rg -n "AiPhase|IMover|IdleAbility|PatrolAbility|AlertAbility|WalkRandom\(\)" Assets/_Game/Scripts` 仅剩预期(如 `Nav.WalkToRandom` 属正常)。
+- [ ] **Step 4: 更新记忆** `ai_decision_only_delegates_abilities.md` / `enemy_nav_movement_architecture.md`:记录 Model A 已被 EnemyLocomotion 取代、执行层单一化、AiPhase 已删。
+
+---
+
+## Self-Review(作者已核对)
+
+- **Spec 覆盖**:§3.1 EnemyLocomotion→P2/Task2;§3.2 AI 用法→P3/Task8-9;§3.3 删 AiPhase→P4;§3.4 Stats→P5;§4 E002-E06 范式→文档(不实现);§5 分期→P2-P6;§6 测试→各 Task 的 EditMode/PlayMode 步骤;§7 飞行缺口→不实现(记录在案)。P1 已完成。
+- **占位符**:无 TBD/TODO;每个代码步给出实际代码;对"实现前需按实际签名 grep 确认"的动态点已显式标注(Pace/Waypoints 依赖 EnemyMovement 现有 API)。
+- **类型一致**:`IEnemyLocomotion`(Task1) 全程一致使用;`LocomotionMode` 值 Idle/Patrol/Face/Approach 贯穿;`Config` 字段 `IdleMode/PatrolMode/AlertMode/ChaseAbilityId`(Task8) 与 E001 用法(Task9) 一致;`FakeLocomotion`(Task6) 与 IAiContext.Locomotion(Task7) 匹配。
+- **已知风险点**:飞行怪走 FlyingDirectNavigator(本计划 E001 为地面怪,不触发飞行分支,飞行接入留待 E002-E06 落地时按 spec §7)。