diff --git a/Docs_Dev/superpowers/plans/2026-07-23-enemy-pathfind-obstruction-handling.md b/Docs_Dev/superpowers/plans/2026-07-23-enemy-pathfind-obstruction-handling.md
new file mode 100644
index 00000000..9cafce79
--- /dev/null
+++ b/Docs_Dev/superpowers/plans/2026-07-23-enemy-pathfind-obstruction-handling.md
@@ -0,0 +1,509 @@
+# 敌人寻路受阻处理 实现计划(Waypoints 巡逻 + MoveTo 一次性目标)
+
+> **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:** 让走 PathBerserker2d 寻路的移动在目标被障碍挡住时「寻路到最近可到达点」而非对着障碍空推,同时覆盖「障碍已进图(寻路失败)」与「障碍未进图(物理卡死)」两种成因。
+
+**Architecture:** 三层——PB2D `NavAgent` 暴露既有的最近可达点;`EnemyNavAgent` 执行层做卡死检测 + 统一受阻处理 + best-effort 改道(可复用);`EnemyLocomotion` 意图层只负责 Waypoints「受阻/到达即结算推进」。设计文档见 `Docs_Dev/superpowers/specs/2026-07-23-enemy-pathfind-obstruction-handling-design.md`。
+
+**Tech Stack:** Unity C#、PathBerserker2d(vendored 源码可改)、Unity MCP(编译/Play 验证)。
+
+**测试策略(重要,TDD 适配):** 本改动全部为 Unity 引擎耦合的运行时逻辑(NavGraph/物理/协程),项目无单元测试工程。因此**不写 NUnit 单测**,改用两道验证门:① 每个代码任务后用 `unity_get_compilation_errors` 确认零编译错误;② 全部完成后在 Play 模式经 Unity MCP 做行为验证(Task 6)。任务顺序已排成**每一步都能独立编译**。
+
+> **Unity MCP 前置:** 首个 MCP 调用会自动发现实例;若多开需先 `unity_select_instance`。所有 MCP 调用走 `unity_*` 工具,勿直连 HTTP 桥。
+
+---
+
+## Task 1: PB2D `NavAgent` 暴露最近可达点
+
+**Files:**
+- Modify: `Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs`
+
+- [ ] **Step 1: 加私有字段**
+
+在字段区 `private bool stopRequested;`(约 `:366`)之后新增一行:
+
+```csharp
+ private bool stopRequested;
+ // [项目新增] 上次寻路失败(NoPathFromStartToGoal)时算出的最近可达点,供执行层"寻路到最近可到达点"读取。
+ private NavSegmentPositionPointer _lastClosestReachable = NavSegmentPositionPointer.Invalid;
+```
+
+- [ ] **Step 2: 加公共读取方法**
+
+在 `CanReach(Vector2 goal)` 方法结束(`return pr.Status == PathRequest.RequestState.Finished; }`,约 `:838`)之后新增:
+
+```csharp
+ ///
+ /// After the most recent failed path request (NoPathFromStartToGoal), returns the closest
+ /// reachable world position found towards the goal. Returns false if none is available.
+ ///
+ public bool TryGetClosestReachablePosition(out Vector2 worldPos)
+ {
+ if (!_lastClosestReachable.IsInvalid())
+ {
+ worldPos = _lastClosestReachable.Position;
+ return true;
+ }
+ worldPos = Vector2.zero;
+ return false;
+ }
+
+ // [项目新增] 记录失败请求的最近可达点后再广播寻路失败,供执行层读取。仅 NoPath 失败才有最近点。
+ private void RaiseFailedToFindPath(PathRequest failedRequest)
+ {
+ _lastClosestReachable = (failedRequest != null
+ && failedRequest.FailReason == PathRequest.RequestFailReason.NoPathFromStartToGoal)
+ ? failedRequest.closestReachablePosition
+ : NavSegmentPositionPointer.Invalid;
+ OnFailedToFindPath?.Invoke(this);
+ }
+```
+
+- [ ] **Step 3: 替换 `HandlePathRequest` 里两处失败广播**
+
+第一处——`Failed` 分支 `!allowCloseEnoughPath` 内(约 `:956`):
+
+```csharp
+ OnFailedToFindPath?.Invoke(this);
+ currentPathRequest.Reset();
+```
+改为:
+```csharp
+ RaiseFailedToFindPath(currentPathRequest);
+ currentPathRequest.Reset();
+```
+
+第二处——`Failed` 分支 `allowCloseEnoughPath` 内(约 `:967`):
+
+```csharp
+ OnFailedToFindPath?.Invoke(this);
+ }
+ break;
+ case PathRequest.RequestState.Finished:
+```
+改为:
+```csharp
+ RaiseFailedToFindPath(currentPathRequest);
+ }
+ break;
+ case PathRequest.RequestState.Finished:
+```
+
+- [ ] **Step 4: 替换 `Repath` 里的失败广播**
+
+`Repath()` 的 `Failed` 分支(约 `:927`):
+
+```csharp
+ Stop();
+ OnFailedToFindPath?.Invoke(this);
+ }
+
+ repathPathRequest.Reset();
+```
+改为:
+```csharp
+ Stop();
+ RaiseFailedToFindPath(repathPathRequest);
+ }
+
+ repathPathRequest.Reset();
+```
+
+- [ ] **Step 5: 编译验证**
+
+调用 `unity_get_compilation_errors`。
+预期:无错误。(`NavSegmentPositionPointer.Position`/`.IsInvalid()`/`.Invalid` 已在本文件使用过,均可访问。)
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs
+git commit -m "feat(nav): NavAgent 暴露寻路失败时的最近可达点 TryGetClosestReachablePosition"
+```
+
+---
+
+## Task 2: `EnemyNavAgent` 卡死检测 + 统一受阻处理 + best-effort 改道
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs`
+
+> 说明:本任务先把两个新成员做成**具体 public 方法/属性**(`LastMoveObstructed`、`TryGetClosestReachable`)。Task 4 才把它们加进 `IPathAgent` 接口——届时本类已满足接口,无需再改。
+
+- [ ] **Step 1: 加序列化调参字段**
+
+在 `_maxSnapDistance` 字段块(约 `:42`)之后、`// 重寻路防抖状态` 之前新增:
+
+```csharp
+ [Header("受阻检测(寻路移动无进展即判被挡)")]
+ [Tooltip("卡死检测窗口时长(s):段上移动时,此时长内朝子目标推进小于阈值即判受阻")]
+ [SerializeField] private float _stuckWindowSeconds = 0.5f;
+ [Tooltip("卡死推进阈值(m):窗口内朝子目标的距离缩小不足此值即判卡死")]
+ [SerializeField] private float _stuckMinProgress = 0.05f;
+```
+
+- [ ] **Step 2: 加受阻/卡死运行时状态字段**
+
+在 `// 连接段状态缓存` 区(`_currentLinkType` 等,约 `:73`)之前或之后新增:
+
+```csharp
+ // 受阻状态(供意图层结算)
+ private bool _lastMoveObstructed;
+ private bool _redirectedToClosest; // 本轮已改道到最近点,避免每帧重复下发
+ private bool _obstructionSignal; // 受阻信号(寻路失败/卡死),延后到 FixedUpdate 统一处理
+ // 卡死检测窗口
+ private float _stuckWinTimer;
+ private float _stuckWinStartDist;
+ private bool _stuckWinInit;
+```
+
+- [ ] **Step 3: 加公共状态成员 + 最近点查询**
+
+在 `public Vector2 CurrentLinkEnd => _currentLinkEnd;`(约 `:54`)之后新增:
+
+```csharp
+ /// 上次寻路移动是否因不可达/被挡而受阻(走到最近点或原地停,而非到达原目标)。新目标 RequestMoveTo 后复位。
+ public bool LastMoveObstructed => _lastMoveObstructed;
+
+ /// 取上次寻路失败时算出的最近可达世界点。无则返回 false。
+ public bool TryGetClosestReachable(out Vector2 worldPos)
+ {
+ if (_navAgent != null) return _navAgent.TryGetClosestReachablePosition(out worldPos);
+ worldPos = Vector2.zero;
+ return false;
+ }
+```
+
+- [ ] **Step 4: `RequestMoveTo` 新目标复位受阻态**
+
+将 `RequestMoveTo`(约 `:145-159`)改为(新增 3 行复位):
+
+```csharp
+ public void RequestMoveTo(Vector2 target)
+ {
+ if (_navAgent == null) return;
+ if (_hasPathGoal
+ && Vector2.Distance(target, _lastPathGoal) < _repathTargetThreshold
+ && Time.time - _lastPathTime < _repathMinInterval)
+ return;
+
+ _lastPathGoal = target;
+ _hasPathGoal = true;
+ _lastPathTime = Time.time;
+ _lastMoveObstructed = false; // 新目标 → 复位受阻态
+ _redirectedToClosest = false;
+ ResetStuckWindow();
+ _navAgent.UpdatePath(target);
+ }
+```
+
+- [ ] **Step 5: `HandlePathFailed` 仅置信号(不在回调内重入寻路)**
+
+将 `private void HandlePathFailed(NavAgent _) => OnNavPathFailed?.Invoke();`(约 `:251`)改为:
+
+```csharp
+ private void HandlePathFailed(NavAgent _)
+ {
+ _obstructionSignal = true; // 延后到 FixedUpdate 统一处理,避免在 NavAgent 失败回调内重入 UpdatePath
+ OnNavPathFailed?.Invoke();
+ }
+```
+
+- [ ] **Step 6: `FixedUpdate` 接入卡死检测 + 受阻处理**
+
+将 `FixedUpdate`(约 `:260-285`)整体替换为:
+
+```csharp
+ private void FixedUpdate()
+ {
+ if (_enemyMovement == null || _navAgent == null) return;
+
+ bool onSegment = _navAgent.IsMovingOnSegment;
+ _enemyMovement.NavDriving = onSegment; // 状态标记(供调试/上层判断"是否导航中")
+
+ TickStuckDetection(onSegment);
+
+ if (onSegment)
+ {
+ // 物理原生:朝子目标水平移动,由 EnemyMovement 施加 velocity、刚体重力保持贴地
+ float dx = _navAgent.PathSubGoal.x - transform.position.x;
+ _enemyMovement.PendingInput.MoveDir = Mathf.Abs(dx) > 0.05f ? Mathf.Sign(dx) : 0f;
+ _enemyMovement.PendingInput.MoveSpeed = _movement != null ? _movement.movementSpeed : 0f;
+
+ // 段推进:到达当前子目标 → 推进到下一段
+ if (_navAgent.HasReachedCurrentSubGoal(0.25f))
+ _navAgent.CompleteSegmentTraversal();
+ }
+ else if (_wasNavOnSegment && !_navAgent.IsFollowingAPath)
+ {
+ // 导航刚结束(到达目标 / 路径失败)→ 停下水平移动
+ _enemyMovement.PendingInput.WantStop = true;
+ }
+
+ // 受阻信号统一处理(寻路失败/卡死):此处不在 NavAgent 回调内,UpdatePath 安全
+ if (_obstructionSignal)
+ {
+ _obstructionSignal = false;
+ ProcessObstruction();
+ }
+
+ _wasNavOnSegment = onSegment;
+ }
+
+ // 卡死检测:段上移动时,滑动窗口内朝子目标推进不足阈值 → 判受阻(覆盖"障碍未进图"的物理空推)。
+ private void TickStuckDetection(bool onSegment)
+ {
+ if (!onSegment) { _stuckWinInit = false; return; }
+
+ float dist = Mathf.Abs(_navAgent.PathSubGoal.x - transform.position.x);
+ if (!_stuckWinInit)
+ {
+ _stuckWinInit = true;
+ _stuckWinTimer = 0f;
+ _stuckWinStartDist = dist;
+ return;
+ }
+ _stuckWinTimer += Time.fixedDeltaTime;
+ if (_stuckWinTimer >= _stuckWindowSeconds)
+ {
+ if (_stuckWinStartDist - dist < _stuckMinProgress)
+ _obstructionSignal = true; // 窗口内几乎没靠近子目标 → 卡死
+ _stuckWinTimer = 0f;
+ _stuckWinStartDist = dist;
+ }
+ }
+
+ private void ResetStuckWindow() => _stuckWinInit = false;
+
+ // 统一受阻处理:尽力改道到最近可达点(走到障碍前),拿不到更近点则停下,交由意图层结算。
+ private void ProcessObstruction()
+ {
+ _lastMoveObstructed = true;
+
+ if (!_redirectedToClosest
+ && _navAgent.TryGetClosestReachablePosition(out var closest)
+ && Vector2.Distance(transform.position, closest) > _repathTargetThreshold)
+ {
+ _redirectedToClosest = true;
+ ResetStuckWindow();
+ _navAgent.UpdatePath(closest); // 走向最近可达点
+ return;
+ }
+
+ // 无更近点 / 已在最近点附近(含"障碍未进图"无 closest 数据)→ 停下
+ _navAgent.Stop();
+ _enemyMovement.PendingInput.WantStop = true;
+ }
+```
+
+- [ ] **Step 7: 编译验证**
+
+调用 `unity_get_compilation_errors`。
+预期:无错误。(`_movement`/`_navAgent`/`PathSubGoal`/`IsMovingOnSegment`/`HasReachedCurrentSubGoal`/`UpdatePath`/`Stop` 均已存在。)
+
+- [ ] **Step 8: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs
+git commit -m "feat(enemy): EnemyNavAgent 卡死检测+统一受阻处理+best-effort 改道到最近可达点"
+```
+
+---
+
+## Task 3: `FlyingDirectNavigator` 实现新成员(返回 false)
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Enemies/Navigation/FlyingDirectNavigator.cs`
+
+- [ ] **Step 1: 加两个成员**
+
+在 IPathAgent 属性区 `public Vector2 CurrentLinkEnd => Vector2.zero;`(约 `:67`)之后新增:
+
+```csharp
+ // 飞行单位直飞、始终可达,无"受阻/最近可达点"概念。
+ public bool LastMoveObstructed => false;
+ public bool TryGetClosestReachable(out Vector2 worldPos) { worldPos = Vector2.zero; return false; }
+```
+
+- [ ] **Step 2: 编译验证**
+
+调用 `unity_get_compilation_errors`。预期:无错误。
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Enemies/Navigation/FlyingDirectNavigator.cs
+git commit -m "feat(enemy): FlyingDirectNavigator 实现受阻/最近可达点成员(飞行单位恒 false)"
+```
+
+---
+
+## Task 4: `IPathAgent` 加接口成员 + `NullPathAgent` 实现
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Enemies/IPathAgent.cs`
+
+> 此时 `EnemyNavAgent`(Task 2)与 `FlyingDirectNavigator`(Task 3)已具备这两个成员,加进接口后即满足。
+
+- [ ] **Step 1: 接口新增成员**
+
+在接口内 `bool ResolveStandablePoint(Vector2 target, out Vector2 standable);`(约 `:78`)之后新增:
+
+```csharp
+ ///
+ /// 上次寻路移动是否因目标不可达/被挡而受阻(走到最近可达点或原地停下,而非到达原目标)。
+ /// 供意图层(Locomotion)判断本次移动应否视为结算/推进。对新目标 RequestMoveTo 后复位。
+ ///
+ bool LastMoveObstructed { get; }
+
+ /// 取上次寻路失败时算出的最近可达世界点(尽力贴近目标的点)。无则返回 false。
+ bool TryGetClosestReachable(out Vector2 worldPos);
+```
+
+- [ ] **Step 2: `NullPathAgent` 实现**
+
+在 `NullPathAgent` 内 `public bool ResolveStandablePoint(...) { standable = target; return false; }`(约 `:110`)之后新增:
+
+```csharp
+ public bool LastMoveObstructed => false;
+ public bool TryGetClosestReachable(out Vector2 worldPos) { worldPos = Vector2.zero; return false; }
+```
+
+- [ ] **Step 3: 编译验证**
+
+调用 `unity_get_compilation_errors`。
+预期:无错误(三个实现者 EnemyNavAgent/FlyingDirectNavigator/NullPathAgent 均已具备成员)。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Enemies/IPathAgent.cs
+git commit -m "feat(enemy): IPathAgent 增加 LastMoveObstructed/TryGetClosestReachable 契约"
+```
+
+---
+
+## Task 5: `EnemyLocomotion` Waypoints 受阻/到达结算,删除无限重发
+
+**Files:**
+- Modify: `Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs`
+
+- [ ] **Step 1: 加路点停顿状态字段**
+
+在 `// Wander 到点停顿状态`(`_wanderPausing`/`_wanderPauseTimer`,约 `:45-46`)之后新增:
+
+```csharp
+ // Waypoints 受阻结算停顿状态
+ private bool _wpPausing;
+ private float _wpPauseTimer;
+```
+
+- [ ] **Step 2: 切模式时复位路点停顿**
+
+`SetMode` 内 `_wanderPausing = false;`(约 `:63`)之后新增一行:
+
+```csharp
+ _wanderPausing = false; // 切换模式清 Wander 停顿态
+ _wpPausing = false; // 同时清 Waypoints 受阻停顿态
+```
+
+- [ ] **Step 3: 替换 Waypoints 分支**
+
+将 `case PatrolStrategy.Waypoints:` 整段(约 `:187-214`,从 `case PatrolStrategy.Waypoints:` 到该 case 的 `break;`)替换为:
+
+```csharp
+ case PatrolStrategy.Waypoints:
+ if (_waypoints == null || _waypoints.Length == 0)
+ {
+ if (!_warnedNoWaypoints)
+ {
+ Debug.LogWarning("[EnemyLocomotion] PatrolStrategy=Waypoints 但未配置 _waypoints,无法巡逻。", this);
+ _warnedNoWaypoints = true;
+ }
+ break;
+ }
+
+ var nav = _enemy.Nav;
+
+ // 到达判定对"吸附后的可达点"做(原始路点可能摆在空中/崖外,永远进不了到达半径)。
+ bool wpArrived = _wpIndex >= 0 &&
+ Vector2.Distance(_enemy.transform.position, _wpResolvedGoal) <= _waypointArriveRadius;
+
+ // 受阻结算:执行层已把敌人带到最近可达点并停下(或原地停) → 视为本路点完成。
+ bool obstructedSettled = nav != null && nav.LastMoveObstructed && !nav.IsMoving;
+
+ if (_wpIndex < 0 || wpArrived || obstructedSettled)
+ {
+ // 受阻结算走短暂停顿(复用 Wander 停顿参数);正常到达/首个点立即推进。
+ if (obstructedSettled && !_wpPausing)
+ {
+ _wpPausing = true;
+ _wpPauseTimer = Random.Range(_wanderPauseMin, Mathf.Max(_wanderPauseMin, _wanderPauseMax));
+ }
+ if (_wpPausing)
+ {
+ _wpPauseTimer -= Time.deltaTime;
+ if (_wpPauseTimer > 0f) break; // 停顿中,原地待机
+ _wpPausing = false;
+ }
+ AdvanceWaypoint();
+ SetWaypointGoal();
+ }
+ else if (nav != null && !nav.IsMoving && !nav.LastMoveObstructed)
+ {
+ // 瞬态:刚出生尚未映射那一帧的 MoveTo 失败 → 重发(RequestMoveTo 自带 0.25s 防抖)。
+ // 仅"未受阻"时重发;受阻由上面 obstructedSettled 分支结算,不再无限重发不可达点。
+ _enemy.MoveTo(_wpResolvedGoal);
+ }
+ break;
+```
+
+- [ ] **Step 4: 编译验证**
+
+调用 `unity_get_compilation_errors`。
+预期:无错误(`nav.LastMoveObstructed` 来自 Task 4 接口;`Random.Range`/`_wanderPauseMin/Max` 已存在)。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs
+git commit -m "feat(enemy): Waypoints 受阻/到达即结算推进,删除对不可达点的无限重发"
+```
+
+---
+
+## Task 6: Play 模式行为验证(经 Unity MCP)
+
+**Files:** 无(验证任务)
+
+- [ ] **Step 1: 打开测试场景**
+
+`unity_scene_open` 打开 `Assets/_Game/Scenes/Testings/TestRoomA.unity`。确认场景内有 NavSurface + 一个 Waypoints 巡逻敌人(`EnemyLocomotion.PatrolStrategy=Waypoints`,至少 2 个路点)。若无,用脚手架 `SceneObjectPlacerTool` 放置敌人并配路点(遵守 CLAUDE.md §2 脚手架规范)。
+
+- [ ] **Step 2: 成因 B 验证——裸障碍**
+
+在两个路点之间放一个挡路的 Collider(高度≥敌人 Height,横跨路径)。`unity_play_mode` 进入 Play。观察(`unity_screenshot_game` 连续截图或 `unity_editor_state` 读位置):
+预期:敌人走到障碍前 → 停顿约 `_wanderPauseMin~Max` 秒 → 推进到下一个路点继续巡逻。**不再对着障碍空推**。
+
+- [ ] **Step 3: 回归——无障碍巡逻**
+
+移除障碍,重进 Play。
+预期:Waypoints 巡逻在各路点间正常往返,行为与改动前一致(正常到达不停顿、立即推进)。
+
+- [ ] **Step 4: 记录结果**
+
+把验证结论(成因 B 通过 / 回归通过)追加到设计文档末尾或开发笔记。成因 A(障碍已进图寻路失败)待后续 Tier-1 软障碍/重烘焙就绪后复测——本次不阻塞。
+
+- [ ] **Step 5: 退出 Play**
+
+`unity_play_mode` 退出 Play 模式。
+
+---
+
+## 自检记录(写计划时已核对)
+
+- **Spec 覆盖**:设计 §3.1→Task1;§3.2→Task2;§3.4→Task3+Task4;§3.3→Task5;§7→Task6。全覆盖。
+- **占位符**:无 TBD/TODO,每个代码步含完整代码。
+- **类型一致**:`TryGetClosestReachablePosition`(NavAgent) / `TryGetClosestReachable`(IPathAgent 实现) / `LastMoveObstructed` 三处命名在各任务间一致;`_obstructionSignal`/`ResetStuckWindow`/`ProcessObstruction` 定义与调用一致。
+- **可编译顺序**:先在实现类(Task2/3)加具体成员,再加接口(Task4),消费方(Task5)最后——每步均可编译。