diff --git a/Docs_Dev/superpowers/plans/2026-07-27-enemy-body-authority.md b/Docs_Dev/superpowers/plans/2026-07-27-enemy-body-authority.md new file mode 100644 index 00000000..d5fcee93 --- /dev/null +++ b/Docs_Dev/superpowers/plans/2026-07-27-enemy-body-authority.md @@ -0,0 +1,526 @@ +# 角色身体碰撞体唯一权威源(IEnemyBody)实现计划 + +> **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:** 把散落在 `EnemyMovement` 里 4 处重复的"身体碰撞体解析"与各处身体尺寸取用,收敛为 `EnemyBase` 持有、经 `IEnemyBody` 接口暴露的唯一权威源。 + +**Architecture:** 新增纯 C# 类 `EnemyBody`(实现 `IEnemyBody`,懒解析碰撞体、实时读 bounds、方向判定抽为 static 纯函数便于单测);`EnemyBase` 持有它并按 `_bodyCollider → 根 Collider2D` 顺序解析,缺失时显式报错;`EnemyMovement` 的 5 处探测与 `EdgeSafeMargin` 全部改走该接口,删除自己的 `_groundCheckCollider` 字段与 `GroundColliderOrSelf()`。 + +**Tech Stack:** Unity 2022.3、C#、NUnit EditMode 测试(Unity Test Runner,经 MCP `unity_testing_run_tests` 运行)。 + +**设计文档:** `Docs_Dev/superpowers/specs/2026-07-27-enemy-body-authority-design.md` + +**通用验证方式(每个 Task 都要做):** +- **编译门**:改 C# 后先 MCP `unity_execute_menu_item("Assets/Refresh")`,等约 8 秒,再 `unity_get_compilation_errors`,预期 0 错误。 +- **测试门**:MCP `unity_testing_run_tests({mode:"EditMode"})` → `unity_testing_get_job(jobId)`,**基线 177 条全绿**(本计划新增测试后基线上升,见各 Task)。 +- **提交**:中文 + 类型前缀(`feat(enemy):` / `refactor(enemy):` / `test(enemy):`)。 +- **重要**:本计划触及所有敌人共用的移动探测路径,**每个 Task 结束都必须跑全量 EditMode**,不能只跑新增测试。 + +**关键既有事实(已核实,实现时可直接依赖):** +- `EnemyBase` 已有 `protected virtual void OnValidate()`(`EnemyBase.cs:758`,`#if UNITY_EDITOR` 内), + 开头是 `if (SuppressValidationWarnings) return;` —— **不要新增第二个 `OnValidate`**。 +- `EnemyBase.Awake` 在 `EnemyBase.cs:531-552` 一带做组件收集与 `Debug.Assert` 校验。 +- `EnemyMovement._groundCheckCollider`(`EnemyMovement.cs:46`)在 `ENM_CaoZhi`(场景)与 + `ENM_ChaoFeng`(预制体)上**实测均为 null** → 删除它不改变现有行为。 +- `EnemyMovement` 目前**不持有** `EnemyBase` 引用,需要新增懒获取。 + +--- + +## 文件结构 + +**新建:** +- `Assets/_Game/Scripts/Enemies/EnemyBody.cs` — `IEnemyBody` 接口 + `EnemyBody` 实现(唯一职责:身体几何查询)。 +- `Assets/Tests/EditMode/Enemies/EnemyBodyTests.cs` — 方向纯函数单测。 + +**修改:** +- `Assets/_Game/Scripts/Enemies/EnemyBase.cs` — `_bodyCollider` 字段、`Body` 属性、`Awake` 校验、扩展既有 `OnValidate`。 +- `Assets/_Game/Scripts/Enemies/EnemyMovement.cs` — 5 处探测改走 `Body`、`EdgeSafeMargin` 改组合、删字段与私有方法。 + +--- + +## Task 1: `IEnemyBody` 接口与 `EnemyBody` 实现(含纯函数单测,TDD) + +**Files:** +- Test: `Assets/Tests/EditMode/Enemies/EnemyBodyTests.cs` +- Create: `Assets/_Game/Scripts/Enemies/EnemyBody.cs` + +- [ ] **Step 1: 先写失败测试** + +创建 `Assets/Tests/EditMode/Enemies/EnemyBodyTests.cs`: +```csharp +using NUnit.Framework; +using BaseGames.Enemies; + +namespace BaseGames.Tests.EditMode.Enemies +{ + /// + /// EnemyBody 方向相关纯函数测试(无场景依赖)。 + /// 这三个函数的左右符号最易写反,故对两个方向与边界相等情形都做断言。 + /// + public class EnemyBodyTests + { + // 身体占据 [2, 4],中心 3,半宽 1 + const float MinX = 2f, MaxX = 4f; + + [Test] + public void FrontEdge_Right_IsMaxX() + => Assert.AreEqual(MaxX, EnemyBody.FrontEdge(MinX, MaxX, 1f)); + + [Test] + public void FrontEdge_Left_IsMinX() + => Assert.AreEqual(MinX, EnemyBody.FrontEdge(MinX, MaxX, -1f)); + + [Test] + public void FrontEdge_ZeroDir_TreatedAsRight() + => Assert.AreEqual(MaxX, EnemyBody.FrontEdge(MinX, MaxX, 0f)); + + [Test] + public void RearEdge_Right_IsMinX() + => Assert.AreEqual(MinX, EnemyBody.RearEdge(MinX, MaxX, 1f)); + + [Test] + public void RearEdge_Left_IsMaxX() + => Assert.AreEqual(MaxX, EnemyBody.RearEdge(MinX, MaxX, -1f)); + + [Test] + public void RearEdge_ZeroDir_TreatedAsRight() + => Assert.AreEqual(MinX, EnemyBody.RearEdge(MinX, MaxX, 0f)); + + // ── BodyPassed:向右 ───────────────────────────────────────────── + [Test] + public void BodyPassed_Right_NotReached_False() + => Assert.IsFalse(EnemyBody.BodyPassed(MinX, MaxX, 5f, 1f)); // 目标在身体右侧外 + + [Test] + public void BodyPassed_Right_OverlappingTarget_False() + => Assert.IsFalse(EnemyBody.BodyPassed(MinX, MaxX, 3f, 1f)); // 目标仍在身体内,未整体越过 + + [Test] + public void BodyPassed_Right_ExactlyAtRearEdge_True() + => Assert.IsTrue(EnemyBody.BodyPassed(MinX, MaxX, 2f, 1f)); // 后缘恰好等于目标 = 已越过 + + [Test] + public void BodyPassed_Right_FullyPassed_True() + => Assert.IsTrue(EnemyBody.BodyPassed(MinX, MaxX, 1f, 1f)); + + // ── BodyPassed:向左 ───────────────────────────────────────────── + [Test] + public void BodyPassed_Left_NotReached_False() + => Assert.IsFalse(EnemyBody.BodyPassed(MinX, MaxX, 1f, -1f)); // 目标在身体左侧外 + + [Test] + public void BodyPassed_Left_OverlappingTarget_False() + => Assert.IsFalse(EnemyBody.BodyPassed(MinX, MaxX, 3f, -1f)); + + [Test] + public void BodyPassed_Left_ExactlyAtRearEdge_True() + => Assert.IsTrue(EnemyBody.BodyPassed(MinX, MaxX, 4f, -1f)); // 后缘(max)恰好等于目标 + + [Test] + public void BodyPassed_Left_FullyPassed_True() + => Assert.IsTrue(EnemyBody.BodyPassed(MinX, MaxX, 5f, -1f)); + } +} +``` + +- [ ] **Step 2: 运行确认失败** + +MCP:`unity_execute_menu_item("Assets/Refresh")` → 等 8 秒 → `unity_get_compilation_errors`。 +预期:**编译失败**,报 `EnemyBody` 不存在(`CS0103`/`CS0246`)。 + +- [ ] **Step 3: 实现 `EnemyBody.cs`** + +创建 `Assets/_Game/Scripts/Enemies/EnemyBody.cs`: +```csharp +using UnityEngine; + +namespace BaseGames.Enemies +{ + /// + /// 角色身体碰撞体的唯一权威查询口。 + /// 几何实时取自碰撞体 bounds(随位置/动画变化,不缓存数值),仅缓存碰撞体引用。 + /// 唯一数据源见 ——其它组件不得自行解析身体碰撞体。 + /// + public interface IEnemyBody + { + Collider2D Collider { get; } + /// 世界空间包围盒;实时查询。 + Bounds Bounds { get; } + float Width { get; } + float Height { get; } + float HalfWidth { get; } + float HalfHeight { get; } + Vector2 Center { get; } + /// 脚底中点 (center.x, min.y)。 + Vector2 FootPoint { get; } + + /// 的前缘 X(dir≥0 取 max.x,否则 min.x)。 + float FrontEdgeX(float dir); + /// 的后缘 X(前缘的相反侧)。 + float RearEdgeX(float dir); + /// + /// 身体是否已**整体**越过世界 X 坐标: + /// dir≥0 时后缘(min.x) ≥ worldX;dir<0 时后缘(max.x) ≤ worldX。 + /// + bool HasPassed(float worldX, float dir); + } + + /// + /// 的实现(纯 C# 类,非 MonoBehaviour)。 + /// 懒解析碰撞体 → 编辑器模式(Gizmo / OnValidate,Awake 未执行)同样可用。 + /// + public sealed class EnemyBody : IEnemyBody + { + private readonly GameObject _owner; + private readonly Collider2D _explicitCollider; + private Collider2D _resolved; + + /// 宿主 GameObject(未显式指定碰撞体时从其上取 Collider2D)。 + /// 显式指定的身体碰撞体;可为 null。 + public EnemyBody(GameObject owner, Collider2D explicitCollider) + { + _owner = owner; + _explicitCollider = explicitCollider; + } + + public Collider2D Collider + { + get + { + if (_explicitCollider != null) return _explicitCollider; + if (_resolved == null && _owner != null) _resolved = _owner.GetComponent(); + return _resolved; + } + } + + /// + /// 碰撞体缺失时返回以宿主位置为中心的零尺寸包围盒。 + /// 该错误已由 在 Awake/OnValidate 显式报出,此处不重复每帧刷屏。 + /// + public Bounds Bounds + { + get + { + var col = Collider; + if (col != null) return col.bounds; + return new Bounds(_owner != null ? _owner.transform.position : Vector3.zero, Vector3.zero); + } + } + + public float Width => Bounds.size.x; + public float Height => Bounds.size.y; + public float HalfWidth => Bounds.extents.x; + public float HalfHeight => Bounds.extents.y; + public Vector2 Center => Bounds.center; + public Vector2 FootPoint { get { var b = Bounds; return new Vector2(b.center.x, b.min.y); } } + + public float FrontEdgeX(float dir) { var b = Bounds; return FrontEdge(b.min.x, b.max.x, dir); } + public float RearEdgeX (float dir) { var b = Bounds; return RearEdge (b.min.x, b.max.x, dir); } + public bool HasPassed(float worldX, float dir) + { + var b = Bounds; + return BodyPassed(b.min.x, b.max.x, worldX, dir); + } + + // ── 方向纯函数(static,便于无场景依赖单测;符号最易写反,集中于此)────── + public static float FrontEdge(float minX, float maxX, float dir) => dir >= 0f ? maxX : minX; + public static float RearEdge (float minX, float maxX, float dir) => dir >= 0f ? minX : maxX; + public static bool BodyPassed(float minX, float maxX, float worldX, float dir) + => dir >= 0f ? minX >= worldX : maxX <= worldX; + } +} +``` + +- [ ] **Step 4: 编译门 + 运行测试确认通过** + +MCP:`Assets/Refresh` → 等 8 秒 → `unity_get_compilation_errors`(预期 0 错误) +→ `unity_testing_run_tests({mode:"EditMode"})` → `unity_testing_get_job(jobId)`。 +预期:**191 条全绿**(原 177 + 新增 14)。 + +- [ ] **Step 5: 提交** + +```bash +git add Assets/_Game/Scripts/Enemies/EnemyBody.cs Assets/Tests/EditMode/Enemies/EnemyBodyTests.cs +git commit -m "feat(enemy): 新增 IEnemyBody 身体几何权威接口与 EnemyBody 实现(+14 纯函数单测)" +``` + +--- + +## Task 2: `EnemyBase` 持有权威源 + +**Files:** +- Modify: `Assets/_Game/Scripts/Enemies/EnemyBase.cs` + +- [ ] **Step 1: 加序列化字段与 `Body` 属性** + +在 `EnemyBase.cs` 的"子组件(Prefab Inspector 绑定)"Header 区块内、`_hurtBox` 字段之后(约 `EnemyBase.cs:44`)追加: +```csharp + + [Header("身体碰撞体(唯一权威源)")] + [Tooltip("用于身体几何(宽高/边缘/探测起点)的碰撞体。留空 = 使用本物体上的 Collider2D。" + + "移动探测、导航内缩、冲刺终点等全部以此为准,不要在各处自行取碰撞体。")] + [SerializeField] private Collider2D _bodyCollider; +``` + +在 `_colliders` 字段声明附近(约 `EnemyBase.cs:70`)追加缓存字段: +```csharp + // 身体几何权威(懒创建;OnValidate 改引用后置空重建) + private EnemyBody _body; +``` + +在公开属性区(`Abilities` / `AttackSelector` 属性附近)追加: +```csharp + /// 身体碰撞体几何的唯一权威查询口。移动/导航/能力一律经此获取身体尺寸。 + public IEnemyBody Body => _body ??= new EnemyBody(gameObject, _bodyCollider); +``` + +- [ ] **Step 2: `Awake` 加运行期校验** + +在 `EnemyBase.Awake` 中 `_colliders = GetComponentsInChildren(true);`(约 `EnemyBase.cs:544`)之后追加: +```csharp + // 身体几何权威校验:解析不到碰撞体 → 显式报错(根因暴露,不静默兜底) + if (Body.Collider == null) + Debug.LogError($"[EnemyBase] {name} 找不到身体碰撞体:请在 Inspector 指定 _bodyCollider," + + "或确保本物体上挂有 Collider2D。移动探测/导航内缩将不可用。", this); +``` + +- [ ] **Step 3: 扩展既有 `OnValidate`(不要新增第二个)** + +`EnemyBase.cs:758` 已有 `protected virtual void OnValidate()`,把它改为: +```csharp + protected virtual void OnValidate() + { + _body = null; // 身体碰撞体引用可能在 Inspector 被改,缓存无条件失效(与警告抑制无关) + if (SuppressValidationWarnings) return; + if (_statsSO == null) + Debug.LogWarning($"[EnemyBase] {gameObject.name} 缺少 EnemyStatsSO 配置(运行时会 NullRef)。", this); + if (_stats == null) + Debug.LogWarning($"[EnemyBase] {gameObject.name} 未绑定 EnemyStats 组件引用。", this); + if (_animancer == null) + Debug.LogWarning($"[EnemyBase] {gameObject.name} 未绑定 AnimancerComponent 引用。", this); + if (_bodyCollider == null && GetComponent() == null) + Debug.LogWarning($"[EnemyBase] {gameObject.name} 找不到身体碰撞体(未指定 _bodyCollider 且本物体无 Collider2D)。", this); + } +``` + +- [ ] **Step 4: 编译门 + 全量测试** + +MCP:`Assets/Refresh` → 等 8 秒 → `unity_get_compilation_errors`(预期 0 错误) +→ `unity_testing_run_tests({mode:"EditMode"})`。预期:**191 条全绿**。 + +- [ ] **Step 5: 提交** + +```bash +git add Assets/_Game/Scripts/Enemies/EnemyBase.cs +git commit -m "feat(enemy): EnemyBase 持有身体几何权威(_bodyCollider + Body),Awake/OnValidate 双重校验" +``` + +--- + +## Task 3: `EnemyMovement` 全部改走权威接口 + +**Files:** +- Modify: `Assets/_Game/Scripts/Enemies/EnemyMovement.cs` + +> 本 Task 是行为等价重构:改完后巡逻/夹停/掉头行为必须与改前完全一致。 + +- [ ] **Step 1: 加 `EnemyBase` 懒获取与 `Body` 便捷属性** + +在 `EnemyMovement` 类的私有字段区追加(放在 `_groundCheckCollider` 附近即可): +```csharp + // 身体几何权威(EnemyBase.Body)。懒获取以支持编辑器模式(Awake 未执行)下的 Gizmo/探测预览。 + private EnemyBase _enemy; + private IEnemyBody Body + { + get + { + if (_enemy == null) _enemy = GetComponent(); + return _enemy != null ? _enemy.Body : null; + } + } +``` + +- [ ] **Step 2: 三个射线起点改用 `Body`** + +把 `EnemyMovement.cs` 中这三个方法整体替换(原实现各自解析碰撞体,约在 185–229 行): +```csharp + private Vector2 GetGroundRayOrigin(int index) + { + var body = Body; + if (body == null || body.Collider == null) return (Vector2)transform.position; + + Bounds b = body.Bounds; + float x = _groundCheckCount <= 1 + ? b.center.x + : Mathf.Lerp(b.min.x, b.max.x, (float)index / (_groundCheckCount - 1)); + return new Vector2(x, b.min.y); + } + + private bool IsGroundedCheck() + { + for (int i = 0; i < _groundCheckCount; i++) + { + if (Physics2D.Raycast(GetGroundRayOrigin(i), Vector2.down, _groundCheckDist, _groundMask)) + return true; + } + return false; + } + + // 墙体射线起点:身体朝向侧边缘、中心高度 + private Vector2 GetWallRayOrigin() + { + var body = Body; + if (body == null || body.Collider == null) return (Vector2)transform.position; + return new Vector2(body.FrontEdgeX(_facingDir), body.Bounds.center.y); + } + + // 悬崖射线起点:身体前下角再向前偏移 _ledgeCheckFwdOffset + private Vector2 GetLedgeRayOrigin() + { + var body = Body; + if (body == null || body.Collider == null) return (Vector2)transform.position; + float sign = _facingDir >= 0 ? 1f : -1f; + return new Vector2(body.FrontEdgeX(_facingDir) + sign * _ledgeCheckFwdOffset, body.Bounds.min.y); + } +``` + +- [ ] **Step 3: 删除 `GroundColliderOrSelf()` 与 `_groundCheckCollider` 字段** + +删除私有方法(原 `EnemyMovement.cs:231-232`): +```csharp + private Collider2D GroundColliderOrSelf() + => _groundCheckCollider != null ? _groundCheckCollider : GetComponent(); +``` +删除序列化字段及其 `[Tooltip]`(原 `EnemyMovement.cs:45-46`): +```csharp + [Tooltip("用于确定射线起点宽度和底边的 Collider2D;留空则 Awake 时自动查找")] + [SerializeField] private Collider2D _groundCheckCollider; +``` + +- [ ] **Step 4: 墙/悬崖判定与 `EdgeSafeMargin` 改用 `Body`** + +替换这三个成员(原 `EnemyMovement.cs:243-278`): +```csharp + /// 前缘水平方向是否有墙。 + public bool WouldHitWallAhead(float dir) + { + var body = Body; + if (body == null || body.Collider == null || _wallCheckDist <= 0f) return false; + float sign = Mathf.Sign(dir); + return Physics2D.Raycast(new Vector2(body.FrontEdgeX(dir), body.Bounds.center.y), + new Vector2(sign, 0f), _wallCheckDist, _wallMask); + } + + /// 前缘外是否为悬崖(脚边地面缺失,会让碰撞体越出地形边缘)。 + public bool WouldFallAhead(float dir) + { + var body = Body; + if (body == null || body.Collider == null || _ledgeCheckDownDist <= 0f) return false; + float sign = Mathf.Sign(dir); + return !Physics2D.Raycast( + new Vector2(body.FrontEdgeX(dir) + sign * _ledgeCheckFwdOffset, body.Bounds.min.y), + Vector2.down, _ledgeCheckDownDist, _groundMask); + } + + /// + /// 身体中心到"能站住的平台边缘最近点"的水平安全余量 = 身体半宽 + 悬崖探测前向偏移。 + /// 半宽取自唯一权威 ;探测偏移是本移动层自己的射线调参。 + /// WouldFallAhead 在"身体前缘 + _ledgeCheckFwdOffset"处探地,故身体中心必须离平台边缘至少此距离, + /// 否则前缘探出崖沿被夹停。供 nav 段内随机游走/目标点吸附把点从段两端内缩此值。 + /// + public float EdgeSafeMargin + { + get + { + var body = Body; + float halfWidth = body != null ? body.HalfWidth : 0f; + return halfWidth + Mathf.Max(0f, _ledgeCheckFwdOffset); + } + } +``` + +- [ ] **Step 5: 确认没有遗漏的旧引用** + +Grep 确认 `_groundCheckCollider` 与 `GroundColliderOrSelf` 在整个项目已无引用: +```bash +grep -rn "_groundCheckCollider\|GroundColliderOrSelf" Assets/_Game/Scripts +``` +预期:**无输出**。若编辑器脚本(如 `EnemyLocomotionEditor`)引用了它们,一并改为 `Body`。 + +- [ ] **Step 6: 编译门 + 全量测试** + +MCP:`Assets/Refresh` → 等 8 秒 → `unity_get_compilation_errors`(预期 0 错误) +→ `unity_testing_run_tests({mode:"EditMode"})`。预期:**191 条全绿**。 + +- [ ] **Step 7: 提交** + +```bash +git add Assets/_Game/Scripts/Enemies/EnemyMovement.cs +git commit -m "refactor(enemy): 移动层探测与 EdgeSafeMargin 改走 Body 权威,删除重复的碰撞体解析" +``` + +--- + +## Task 4: PlayMode 行为等价性冒烟验证 + +**Files:** 无(场景内验证) + +> 因两个现存敌人的 `_groundCheckCollider` 实测均为 null,本次重构后行为应**完全一致**。 +> 本 Task 用于确认没有引入回归。 + +- [ ] **Step 1: 确认编辑器无新报错** + +MCP `unity_console_log({type:"error", count:30})`:确认没有新增的 +`[EnemyBase] ... 找不到身体碰撞体` 报错(`ENM_CaoZhi` 有根 BoxCollider2D,不该触发)。 + +- [ ] **Step 2: 进 Play 验证巡逻与夹停** + +MCP `unity_play_mode({action:"play"})`,等约 5 秒后用 `unity_execute_code` 读取状态: +```csharp +var e = GameObject.Find("ENM_CaoZhi"); +var mv = e.GetComponent(); +var eb = e.GetComponent(); +return "grounded=" + mv.IsGrounded + + "; halfWidth=" + eb.Body.HalfWidth.ToString("F3") + + "; edgeSafeMargin=" + mv.EdgeSafeMargin.ToString("F3") + + "; bodyCollider=" + (eb.Body.Collider != null ? eb.Body.Collider.GetType().Name : "NULL") + + "; bounds=" + eb.Body.Bounds.ToString(); +``` +预期:`bodyCollider=BoxCollider2D`(非 NULL)、`halfWidth>0`、`edgeSafeMargin = halfWidth + 0.1`、 +`grounded=True`(敌人站在平台上)。 + +- [ ] **Step 3: 验证敌人仍会被崖边夹停、不会掉下平台** + +保持 Play,观察 `ENM_CaoZhi` 巡逻若干秒后再读一次: +```csharp +var e = GameObject.Find("ENM_CaoZhi"); +var mv = e.GetComponent(); +return "pos=" + e.transform.position.ToString("F2") + + "; grounded=" + mv.IsGrounded + + "; wallAhead=" + mv.IsWallAhead + + "; ledgeAhead=" + mv.IsLedgeAhead; +``` +预期:`grounded=True`(未掉出平台);敌人位置在平台范围内。 + +- [ ] **Step 4: 退出 Play** + +MCP `unity_play_mode({action:"stop"})`。确认 Console 无新增异常。 + +- [ ] **Step 5: 记录结果** + +若全部通过,在提交信息或 `Docs_Dev/Verification/` 记一行验证结论。无代码改动则无需提交。 + +--- + +## 自检记录(写计划时) + +- **Spec 覆盖**:spec §3.1 权威源→Task 2;§3.2 接口 + §3.3 实现→Task 1;§3.4 消费端改造表(`EnemyMovement` + 5 处 + `EdgeSafeMargin` + 删字段/方法)→Task 3;§3.5 分层原则→Task 3 Step 4 的 `EdgeSafeMargin` 注释体现; + §4 边界与错误处理→Task 2 Step 2/3(Awake LogError + OnValidate 警告 + 抑制机制)、Task 1 Step 3(碰撞体缺失退化值); + §5 测试→Task 1(单测)、Task 3 Step 6(回归)、Task 4(PlayMode);§6 迁移安全→Task 4 验证。 + **`EnemyNavAgent` 按 spec §3.4 明确不改**,故无对应 Task。 +- **占位符扫描**:无 TBD/TODO;每个改动步骤都给了完整代码。 +- **类型一致性**:`IEnemyBody` 成员(`Collider`/`Bounds`/`HalfWidth`/`FrontEdgeX`/`RearEdgeX`/`HasPassed`) + 与 static 纯函数(`FrontEdge`/`RearEdge`/`BodyPassed`)在 Task 1 定义,Task 2(`Body.Collider`)、 + Task 3(`Body.Bounds`/`Body.FrontEdgeX`/`Body.HalfWidth`)与测试中的用法一致。 +- **测试基线**:当前 177 → Task 1 后 191(新增 14 条);Task 2/3 不新增测试,仍应为 191。