diff --git a/Docs/superpowers/plans/2026-07-03-braingraph-phase-enemy-integration.md b/Docs/superpowers/plans/2026-07-03-braingraph-phase-enemy-integration.md new file mode 100644 index 00000000..59c8dc56 --- /dev/null +++ b/Docs/superpowers/plans/2026-07-03-braingraph-phase-enemy-integration.md @@ -0,0 +1,634 @@ +# BrainGraph 接通敌人阶段(E001 + 脚手架一键)实现计划 + +> **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:** 让脚手架一键放置的 E001(草蛭)敌人,其 AI 完全由自研 BrainGraph 驱动、且**不挂任何 `BehaviorTree` 组件**:新增按 id 解析共享图的注册表、把 EnemyBase 子系统包成能力接口的上下文、承载 `AiRuntime` 的 `EnemyAiBrain` 组件、E001 的 AiScript 定义,移除 EnemyBase 的 Opsive BT 集成,并在 `SceneObjectPlacerTool` 里自动挂载/绑定。 + +**Architecture:** `EnemyAiBrain : MonoBehaviour` 持 `_definitionId`(string),`Awake` 经 `AiDefinitionRegistry.GetGraph(id)` 取每类型共享的不可变 `AiGraph`,构造 `EnemyBrainContext`(实现 `IAiContext`+四能力接口,委托 EnemyBase 子系统)与 `AiRuntime`,`Update` 里 `Tick`。EnemyBase 只在死亡处 `Send(AiSignal.Died)`、`OnSpawn` 调 `ResetBrain()`,受击让位由 `IsControllable` 门自动处理。脚手架把 `EnemyAiBrain` 挂上并 `AssignString(_definitionId,"E001")`。 + +**Tech Stack:** Unity 2022.3(C# 9), Unity Test Framework (EditMode), asmdef, Addressables, MCP(编译/测试/PlayMode 验证)。 + +**依赖:** BrainGraph 第 1 阶段核心运行时已合并 master(`BaseGames.AI`:AiGraph/BrainBuilder/AiRuntime/AiScript/[AiDefinition]/IAiContext/4 接口/Blackboard)。 + +**Spec:** `Docs/superpowers/specs/2026-07-03-enemy-ai-framework-design.md`(本阶段覆盖 §4 适配、§7 组件与脚手架、§8 迁移第 1-2 步的 E001 部分;LOD/AiScheduler、能力编排 sugar、Boss、参数 SO 热调、批量删 BD/Opsive 仍属后续)。 + +--- + +## 关键前提(来自代码探查,务必按真实结构对接) + +- `SceneObjectPlacerTool.PlaceE001_CaoZhi(EnemyBodyColliderType)`(`Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs:439`)用 `GetOrAddComponent`、`AssignReference`/`AssignAsset`/`AssignString`(该文件内 private static,见 :2204/:2244/:2257/:2333)装配敌人;末尾 `report.Add("★ 挂载行为树…E001_CaoZhi.asset。")`(**:524**)是要替换的接线点。E001 已带 `HurtBox` + `ContactDamageZone`(`BodyContactDamage`)——**接触即伤害**,无需攻击能力。 +- `EnemyBase`(`Assets/_Game/Scripts/Enemies/EnemyBase.cs`)暴露:`IsPlayerVisible()`、`IsPlayerInRange(float)`、`PlayerTransform`、`Nav`(`IPathAgent`,含 `IsMoving`/`WalkToRandom`)、`MoveTo(Vector2)`、`FacePlayer()`、`StopMovement()`、`BeginLookAround()`、`Abilities`(`EnemyAbilityRegistry`)、`IsAlive`、`CurrentState`(`EnemyStateType`)、`Stats`(`EnemyStats`, `CurrentHP`)、`StatsSO`(`MaxHP`)。`EnemyStateType{Controlled,Hurt,Stagger,KnockUp,Dead}`。`IPoolable` 只有 `OnSpawn`/`OnDespawn`。 +- `EnemyBase` 的 Opsive BT 集成全部在 `#if GRAPH_DESIGNER` 内(精确行号见 Task 5)。 +- 已关 Domain Reload:静态注册表须 `RuntimeInitializeOnLoadMethod` 重置。 + +## 文件结构(本阶段) + +新增(`BaseGames.AI`,`Assets/_Game/Scripts/AI/`): +- `AiDefinitionRegistry.cs` — 反射按 id 提供共享 AiGraph。 + +新增(`BaseGames.Enemies`,`Assets/_Game/Scripts/Enemies/AIBrain/`): +- `EnemyBrainContext.cs` — 实现 IAiContext+4 接口,委托 EnemyBase 子系统。 +- `EnemyAiBrain.cs` — MonoBehaviour,承载 AiRuntime。 +- `Ai/E001CaoZhiAi.cs` — `[AiDefinition("E001")]` 的 AiScript。 + +修改: +- `Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef` — references 增加 `BaseGames.AI`。 +- `Assets/_Game/Scripts/Enemies/EnemyBase.cs` — 移除 BT 集成,加 brain 引用/Send(Died)/OnSpawn 重置。 +- `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs` — `PlaceE001_CaoZhi` 挂 `EnemyAiBrain` + 绑 id。 + +测试(`Assets/Tests/EditMode/AI/`): +- `AiDefinitionRegistryTests.cs`。 + +> 命名/注释禁参考游戏名(CLAUDE.md §4);根因暴露、禁下游兜底(§6);创建走脚手架(§2)。 + +--- + +## Task 1: `AiDefinitionRegistry`(反射按 id 取共享图) + +**Files:** +- Create: `Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs` +- Create: `Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs` + +- [ ] **Step 1: 写失败测试 `AiDefinitionRegistryTests.cs`** + +```csharp +using NUnit.Framework; +using BaseGames.AI; + +namespace BaseGames.Tests.EditMode.AI +{ + // 顶层测试定义,供反射注册表发现 + [AiDefinition("__test_grunt")] + internal sealed class TestGruntAi : AiScript + { + protected override void Build(BrainBuilder b) + { + b.Entry("A"); + b.State("A").To("B").When(c => c.Sensor.SeesPlayer(), "SeesPlayer"); + b.State("B"); + } + } + + public class AiDefinitionRegistryTests + { + [Test] + public void Has_FindsRegisteredDefinition() + { + Assert.IsTrue(AiDefinitionRegistry.Has("__test_grunt")); + } + + [Test] + public void GetGraph_ResolvesById() + { + var g = AiDefinitionRegistry.GetGraph("__test_grunt"); + Assert.AreEqual("A", g.EntryState); + } + + [Test] + public void GetGraph_SameId_ReturnsSharedInstance() + { + Assert.AreSame( + AiDefinitionRegistry.GetGraph("__test_grunt"), + AiDefinitionRegistry.GetGraph("__test_grunt")); + } + + [Test] + public void GetGraph_UnknownId_Throws() + { + Assert.Throws( + () => AiDefinitionRegistry.GetGraph("__nope__")); + } + } +} +``` + +- [ ] **Step 2: 运行确认失败** + +Run: EditMode `AiDefinitionRegistryTests`(经 MCP:`unity_advanced_tool` → `unity_testing_run_tests` testMode=EditMode)。 +Expected: 编译失败 —— `AiDefinitionRegistry` 未定义。 + +- [ ] **Step 3: 实现 `AiDefinitionRegistry.cs`** + +```csharp +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace BaseGames.AI +{ + /// + /// 反射收集所有 [AiDefinition] 的 AiScript 子类,按 id 提供共享 AiGraph(flyweight)。 + /// 同一 id 全实例共享同一 AiScript 实例 → 同一张不可变 AiGraph。 + /// + public static class AiDefinitionRegistry + { + static Dictionary _byId; + + // 项目已关闭 Domain Reload:静态缓存需在进入播放时重置,保证按最新类型重建。 + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + static void ResetOnPlay() => _byId = null; + + static void EnsureBuilt() + { + if (_byId != null) return; + _byId = new Dictionary(); + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + Type[] types; + try { types = asm.GetTypes(); } + catch (ReflectionTypeLoadException e) { types = e.Types; } + if (types == null) continue; + for (int i = 0; i < types.Length; i++) + { + var t = types[i]; + if (t == null || t.IsAbstract || !typeof(AiScript).IsAssignableFrom(t)) continue; + var attr = t.GetCustomAttribute(); + if (attr == null) continue; + if (_byId.ContainsKey(attr.Id)) + throw new InvalidOperationException( + $"AiDefinitionRegistry: 重复的 AI 定义 id '{attr.Id}'({t.FullName})。"); + _byId[attr.Id] = (AiScript)Activator.CreateInstance(t); + } + } + } + + public static bool Has(string id) + { + EnsureBuilt(); + return _byId.ContainsKey(id); + } + + /// 取 id 对应的共享 AiGraph;不存在则显式抛错(根因暴露,不做兜底)。 + public static AiGraph GetGraph(string id) + { + EnsureBuilt(); + if (!_byId.TryGetValue(id, out var script)) + throw new InvalidOperationException( + $"AiDefinitionRegistry: 未找到 AI 定义 id '{id}'。请确认存在 [AiDefinition(\"{id}\")] 的 AiScript 子类。"); + return script.GetOrBuildGraph(); + } + + public static IEnumerable Ids + { + get { EnsureBuilt(); return _byId.Keys; } + } + } +} +``` + +- [ ] **Step 4: 运行确认通过** + +Run: EditMode `AiDefinitionRegistryTests` +Expected: PASS(4 项)。全量测试无失败。 + +- [ ] **Step 5: 提交** + +```bash +git add Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs.meta \ + Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs.meta +git commit -m "feat(ai): AiDefinitionRegistry——反射按id提供共享AiGraph(关域重载重置)" +``` + +--- + +## Task 2: `BaseGames.Enemies` 引用 `BaseGames.AI` + `EnemyBrainContext` + +**Files:** +- Modify: `Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef` +- Create: `Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs` + +无独立单测(依赖 EnemyBase MonoBehaviour,行为由 Task 7 集成验证);本任务编译校验。 + +- [ ] **Step 1: 给 `BaseGames.Enemies.asmdef` 的 references 增加 `"BaseGames.AI"`** + +先 Read `Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef`,在 `references` 数组里加入一行 `"BaseGames.AI"`(放在 `"BaseGames.Core"` 之后即可)。其余字段不动。 + +- [ ] **Step 2: 创建 `Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs`** + +```csharp +using UnityEngine; +using BaseGames.AI; + +namespace BaseGames.Enemies +{ + /// + /// 把 EnemyBase 的子系统包装成 BrainGraph 的能力接口(每敌人一实例)。 + /// 状态逻辑只依赖这些接口,不直接引用具体子系统。 + /// + public sealed class EnemyBrainContext : IAiContext, ISensor, IMover, ICombatant, IActorVitals + { + readonly EnemyBase _enemy; + readonly Blackboard _blackboard = new Blackboard(); + Vector2 _lastKnown; + float _lostTimer; + + public EnemyBrainContext(EnemyBase enemy) { _enemy = enemy; } + + // ---- IAiContext ---- + public ISensor Sensor => this; + public IMover Mover => this; + public ICombatant Combat => this; + public IActorVitals Vitals => this; + public Blackboard Blackboard => _blackboard; + + /// 每帧由 EnemyAiBrain 在 Tick 之前调用,维护最后已知位置与丢失计时。 + public void Refresh(float dt) + { + if (_enemy.IsPlayerVisible() && _enemy.PlayerTransform != null) + { + _lastKnown = _enemy.PlayerTransform.position; + _lostTimer = 0f; + } + else + { + _lostTimer += dt; + } + } + + /// 对象池复用时清空临时态。 + public void ResetScratch() + { + _blackboard.Clear(); + _lastKnown = _enemy.transform.position; + _lostTimer = 0f; + } + + // ---- ISensor ---- + public bool SeesPlayer() => _enemy.IsPlayerVisible(); + public bool InRange(float range) => _enemy.IsPlayerInRange(range); + public bool LostFor(float seconds) => !_enemy.IsPlayerVisible() && _lostTimer >= seconds; + public Vector2 LastKnown => _lastKnown; + + // ---- IMover ---- + public void MoveTo(Vector2 target) => _enemy.MoveTo(target); + public void FacePlayer() => _enemy.FacePlayer(); + public void Stop() => _enemy.StopMovement(); + public void WalkRandom() + { + var nav = _enemy.Nav; + if (nav != null && !nav.IsMoving) nav.WalkToRandom(); + } + public void LookAround() => _enemy.BeginLookAround(); + + // ---- ICombatant ---- + public bool UseAbility(string abilityId) + { + var a = _enemy.Abilities?.Get(abilityId); + return a != null && a.Execute(); + } + public bool IsAbilityRunning(string abilityId = null) + { + var reg = _enemy.Abilities; + if (reg == null) return false; + if (abilityId != null) + { + var a = reg.Get(abilityId); + return a != null && a.IsRunning; + } + var all = reg.All; + for (int i = 0; i < all.Count; i++) + if (all[i].IsRunning) return true; + return false; + } + public bool NoAbilityRunning => !IsAbilityRunning(); + public bool CanUseAbility(string abilityId) + { + var a = _enemy.Abilities?.Get(abilityId); + return a != null && a.CanUse; + } + + // ---- IActorVitals ---- + public bool IsAlive => _enemy.IsAlive; + public bool IsControllable => _enemy.CurrentState == EnemyStateType.Controlled; + public float HpPercent + { + get + { + var so = _enemy.StatsSO; + return so != null && so.MaxHP > 0 ? (float)_enemy.Stats.CurrentHP / so.MaxHP : 0f; + } + } + public bool HpBelow(float ratio) => HpPercent < ratio; + } +} +``` + +> 若某个成员的 EnemyBase 属性/方法名与上面不符(如 `Nav` 无 `IsMoving`),**先 Read `EnemyBase.cs`/`IPathAgent.cs`/`EnemyAbilityRegistry.cs` 核对真实签名再调整**——不要臆造。`EnemyStateType` 若是 `EnemyBase` 的嵌套类型,则写 `EnemyBase.EnemyStateType.Controlled`。 + +- [ ] **Step 3: 刷新并确认编译零错误** + +`unity_execute_menu_item`("Assets/Refresh") → `unity_get_compilation_errors`(severity 'error') = 0。 + +- [ ] **Step 4: 提交** + +```bash +git add Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef \ + Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs.meta \ + Assets/_Game/Scripts/Enemies/AIBrain.meta +git commit -m "feat(ai): EnemyBrainContext——EnemyBase子系统适配为BrainGraph能力接口" +``` + +--- + +## Task 3: `EnemyAiBrain` 组件 + +**Files:** +- Create: `Assets/_Game/Scripts/Enemies/AIBrain/EnemyAiBrain.cs` + +- [ ] **Step 1: 创建 `EnemyAiBrain.cs`** + +```csharp +using UnityEngine; +using BaseGames.AI; + +namespace BaseGames.Enemies +{ + /// + /// 把 BrainGraph 决策层挂到敌人上:按 id 取共享 AiGraph、构造 AiRuntime、逐帧推进。 + /// 取代旧的 Opsive BehaviorTree 组件。 + /// + [DisallowMultipleComponent] + [RequireComponent(typeof(EnemyBase))] + public sealed class EnemyAiBrain : MonoBehaviour + { + [Tooltip("对应 [AiDefinition(id)] 的敌人 AI 定义 id,例如 E001")] + [SerializeField] string _definitionId; + + EnemyBase _enemy; + EnemyBrainContext _context; + AiRuntime _runtime; + + public string DefinitionId => _definitionId; + public string CurrentStateName => _runtime != null ? _runtime.CurrentStateName : "(none)"; + public bool IsSuspended => _runtime != null && _runtime.IsSuspended; + public AiRuntime Runtime => _runtime; + + void Awake() + { + _enemy = GetComponent(); + if (string.IsNullOrEmpty(_definitionId)) + { + // 根因暴露:漏配 id 直接报错,不静默兜底。 + Debug.LogError($"EnemyAiBrain 未配置 _definitionId:{name}", this); + enabled = false; + return; + } + var graph = AiDefinitionRegistry.GetGraph(_definitionId); // 不存在则抛异常 + _context = new EnemyBrainContext(_enemy); + _runtime = new AiRuntime(graph, _context); + } + + void Update() + { + if (_runtime == null) return; + float dt = Time.deltaTime; + _context.Refresh(dt); + _runtime.Tick(dt); + } + + /// 对象池复用时由 EnemyBase.OnSpawn 调用:回到 Entry、清临时态。 + public void ResetBrain() + { + _context?.ResetScratch(); + _runtime?.Reset(); + } + + /// 信号入口,供 EnemyBase 转发死亡等事件到决策层。 + public void Send(AiSignal signal) => _runtime?.Send(signal); + } +} +``` + +> 设计说明:本阶段每帧 Tick(不做 LOD 节流)——LOD/错峰调度属后续 AiScheduler 阶段。`AiRuntime.Reset()` 已清 Blackboard,但 `EnemyBrainContext` 的 `_lastKnown/_lostTimer` 另需 `ResetScratch()` 清(`AiRuntime` 不知道 context 内部字段)。 + +- [ ] **Step 2: 刷新并确认编译零错误** + +`unity_execute_menu_item`("Assets/Refresh") → `unity_get_compilation_errors`(error) = 0。 + +- [ ] **Step 3: 提交** + +```bash +git add Assets/_Game/Scripts/Enemies/AIBrain/EnemyAiBrain.cs Assets/_Game/Scripts/Enemies/AIBrain/EnemyAiBrain.cs.meta +git commit -m "feat(ai): EnemyAiBrain 组件——承载 AiRuntime、逐帧推进、池复用重置、信号入口" +``` + +--- + +## Task 4: E001 的 AiScript 定义 + +**Files:** +- Create: `Assets/_Game/Scripts/Enemies/AIBrain/Ai/E001CaoZhiAi.cs` + +- [ ] **Step 1: 创建 `E001CaoZhiAi.cs`** + +```csharp +using BaseGames.AI; + +namespace BaseGames.Enemies +{ + /// + /// E001(草蛭)的 AI 定义:巡逻 → 追击(接触即伤害,无需攻击能力)→ 搜查 → 巡逻。 + /// 数值目前用字面量常量;后续可外挂 SO 做热调。 + /// + [AiDefinition("E001")] + public sealed class E001CaoZhiAi : AiScript + { + const float ContactRange = 1.2f; // 贴近判定(仅用于 Chase→Search 前的接近,接触伤害由 BodyContactDamage 触发) + const float LoseTimeout = 2.5f; // 丢失玩家判定 + const float SearchTime = 3f; // 搜查后返回巡逻 + + protected override void Build(BrainBuilder b) + { + b.Entry("Patrol"); + + // 死亡:全局事件,终止决策 + b.Global().To("Dead").OnEvent(AiSignal.Died); + + b.State("Patrol") + .Tick(c => c.Mover.WalkRandom()) + .To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer"); + + b.State("Chase") + .OnEnter(c => c.Mover.FacePlayer()) + .Tick(c => c.Mover.MoveTo(c.Sensor.LastKnown)) // 冲向玩家,接触即由 BodyContactDamage 造成伤害 + .To("Search").When(c => c.Sensor.LostFor(LoseTimeout), "LostFor"); + + b.State("Search") + .OnEnter(c => c.Mover.LookAround()) + .To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer") + .To("Patrol").After(SearchTime); + + b.State("Dead") + .OnEnter(c => c.Mover.Stop()); + } + } +} +``` + +> `ContactRange` 常量暂未用于转换(接触伤害由碰撞触发);保留供后续若加"贴脸减速/停顿"用。如实现时发现 E001 需要一个显式攻击能力,再加 Combat 态与 `c.Combat.UseAbility(...)`(先 Read E001 prefab 的 Abilities 子节点确认真实 abilityId)。 + +- [ ] **Step 2: 刷新并确认编译零错误;确认注册表能发现 E001** + +`unity_execute_menu_item`("Assets/Refresh") → `unity_get_compilation_errors`(error) = 0。 +用 `unity_execute_code`(port 7890) 执行断言:`BaseGames.AI.AiDefinitionRegistry.Has("E001")` 应为 true,`GetGraph("E001").EntryState` 应为 `"Patrol"`。 + +- [ ] **Step 3: 提交** + +```bash +git add Assets/_Game/Scripts/Enemies/AIBrain/Ai/E001CaoZhiAi.cs Assets/_Game/Scripts/Enemies/AIBrain/Ai/E001CaoZhiAi.cs.meta \ + Assets/_Game/Scripts/Enemies/AIBrain/Ai.meta +git commit -m "feat(ai): E001(草蛭) AiScript 定义——巡逻/追击/搜查(接触伤害杂兵)" +``` + +--- + +## Task 5: EnemyBase 移除 Opsive BT 集成 + 接 EnemyAiBrain + +**Files:** +- Modify: `Assets/_Game/Scripts/Enemies/EnemyBase.cs` + +这是本阶段最需谨慎的改动。**务必先完整 Read `EnemyBase.cs` 确认每个 `#if GRAPH_DESIGNER` 块与目标方法的真实当前内容与行号**(下面行号为探查快照,可能因前面提交而漂移)。逐块处理、每步后刷新确认零编译错误。 + +- [ ] **Step 1: 先 Read `EnemyBase.cs`,定位所有 `#if GRAPH_DESIGNER ... #endif` 块**(用 Grep `GRAPH_DESIGNER`)。预期位置:using(~:8-10)、LOD 间隔字段(~:60-72)、调试字段(~:135-137)、`BehaviorTree` 属性(~:257-259)、`SetAggroTickRate`(~:376-380)、`SetAiPhase` 内 BT 间隔 switch(~:471-483)、Awake 挂树(~:606-618)、Update tick(~:629-648)、Update 调试写(~:654-656)、BT 字段(~:697-702)、`StopBehaviorTree()`(~:709-714)、OnSpawn(~:807-809)、OnDespawn(~:823-825)。 + +- [ ] **Step 2: 删除所有 Opsive/BT 专属代码** + - 删 `#if GRAPH_DESIGNER using Opsive...#endif`。 + - 删 `_btIdleTickInterval`…`_btCombatTickInterval` 等 LOD 间隔字段与 `_dbg_BtTickInterval` 调试字段。 + - 删 `public BehaviorTree BehaviorTree => _behaviorTree;` 属性。 + - 删 Awake 里 `#if GRAPH_DESIGNER … _behaviorTree = GetComponent() … #endif` 整块。 + - 删 Update 里 `#if GRAPH_DESIGNER … _behaviorTree.Tick() … #endif` 整块与其后调试写行。 + - 删 `_behaviorTree/_btManualMode/_btTickTimer/_btCurrentInterval` 字段。 + - 删 `StopBehaviorTree()` 方法**体内**的 BT 调用;见 Step 4 关于该 public 方法的处理。 + - 删 OnSpawn/OnDespawn 里的 `#if GRAPH_DESIGNER … _behaviorTree… #endif` 块(OnSpawn 的替换见 Step 4)。 + - `SetAggroTickRate(bool)`:删其 BT 间隔赋值;若方法体因此为空,保留为空方法体并加注释 `// LOD tick 速率控制迁移到后续 AiScheduler 阶段`(不要删方法,避免改动调用方)。 + - `SetAiPhase(...)`:只删其中设置 `_btCurrentInterval` 的 switch;**保留** `_currentAiPhase` 赋值、`OnAiPhaseChanged` 广播、阶段动画等其余逻辑。 + +- [ ] **Step 3: 加 EnemyAiBrain 引用与 Awake 获取** + - 加字段:`private EnemyAiBrain _brain;` + - 在 Awake 的子系统收集处(如 `_abilities.CollectFrom(gameObject);` 附近)加:`_brain = GetComponent();` + +- [ ] **Step 4: 死亡发信号 + 池复用重置** + - 在 `PerformDeath()`(死亡结算处,原先调用 `StopBehaviorTree`/`ForceState(Dead)` 的地方)加:`_brain?.Send(BaseGames.AI.AiSignal.Died);` + - `StopBehaviorTree()` 现无 BT 可停:把方法体改为 `_brain?.Send(BaseGames.AI.AiSignal.Died);`(保留方法名,因 `EnemyDeathSequence` 等外部调用它;语义变为"通知决策层终止")。若 Step 4 已在 PerformDeath 直接 Send,则 `StopBehaviorTree` 保留同一 Send 以兼容既有调用点即可(幂等,重复 Died 事件不会二次转换)。 + - 在 `OnSpawn()`(原 `#if GRAPH_DESIGNER _behaviorTree?.StartBehavior(); #endif` 处)替换为:`_brain?.ResetBrain();` + +- [ ] **Step 5: 逐步刷新并确认零编译错误** + +每删/改一处后 `unity_execute_menu_item`("Assets/Refresh") → `unity_get_compilation_errors`(error)。全部处理完应为 **0 error**。注意:删除 BT 字段后若有残留引用(如 debug inspector),一并清理。 + +- [ ] **Step 6: 跑全量 EditMode 测试确认无回归** + +`unity_advanced_tool` → `unity_testing_run_tests`(EditMode) → `unity_testing_get_job`:应无失败(此改动不涉及 EditMode 测试,但确认未破坏编译面)。 + +- [ ] **Step 7: 提交** + +```bash +git add Assets/_Game/Scripts/Enemies/EnemyBase.cs +git commit -m "refactor(ai): EnemyBase 移除 Opsive BT 集成,改接 EnemyAiBrain(Died信号/OnSpawn重置)" +``` + +--- + +## Task 6: 脚手架一键挂载 `EnemyAiBrain`(`SceneObjectPlacerTool`) + +**Files:** +- Modify: `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs` + +- [ ] **Step 1: 定位 `PlaceE001_CaoZhi` 里的行为树 TODO 提示行** + +先 Read 该方法(~:439-530),找到 `report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E001_CaoZhi.asset。");`(~:524)。 + +- [ ] **Step 2: 用真实挂载替换该提示行** + +把该 `report.Add("★ …")` 行替换为(放在 `SetupPerceptionSystemSlots(...)` 之后、方法收尾之前): + +```csharp + // BrainGraph AI:挂载决策组件并绑定定义 id(取代旧的 Opsive BehaviorTree) + var brain = GetOrAddComponent(go); + AssignString(brain, "_definitionId", "E001", report); + report.Add("✔ 已挂载 EnemyAiBrain 并绑定 AI 定义 id=E001(BrainGraph)。"); +``` + +> `EnemyAiBrain` 在 `BaseGames.Enemies`,`SceneObjectPlacerTool` 是 Editor 程序集,需确认 Editor asmdef 已引用 `BaseGames.Enemies`(放置工具本就 new/操作 EnemyBase,故必然已引用——若编译报找不到类型,检查 Editor asmdef references)。文件顶部按需加 `using BaseGames.Enemies;`。`AssignString` 为该文件内既有 private static 辅助(~:2333)。 + +- [ ] **Step 3: 刷新并确认编译零错误** + +`unity_execute_menu_item`("Assets/Refresh") → `unity_get_compilation_errors`(error) = 0。 + +- [ ] **Step 4: 提交** + +```bash +git add Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs +git commit -m "feat(ai): 脚手架 PlaceE001 一键挂载 EnemyAiBrain 并绑定 id=E001" +``` + +--- + +## Task 7: 集成验证(脚手架一键 → PlayMode 驱动 → 无 BehaviorTree) + +**Files:** 无(经 MCP 驱动 Unity 验证;如需可加临时验证脚本但完成后删除)。 + +- [ ] **Step 1: 准备一个干净场景并确认活动场景** + +用 MCP:`unity_scene_new`(或打开一个测试场景)。**务必确认活动场景正确**(避免污染其他场景,见项目约束)。记录场景名。 + +- [ ] **Step 2: 脚手架一键放置 E001** + +用 MCP `unity_execute_menu_item`(menuPath `"BaseGames/Scene/Place/Enemy E001 (草蛭)"`, port 7890)。 +然后 `unity_scene_hierarchy` / `unity_gameobject_info` 找到新建的 `ENM_CaoZhi`。 + +- [ ] **Step 3: 静态断言——挂了 EnemyAiBrain、绑了 id、且无 BehaviorTree** + +用 `unity_component_get_properties`(或 `unity_gameobject_info`)确认 `ENM_CaoZhi`: +- 有 `EnemyAiBrain` 组件,其 `_definitionId == "E001"`。 +- **没有** `BehaviorTree`(Opsive)组件。 + +- [ ] **Step 4: PlayMode 冒烟——决策层随玩家远近推进** + +- 确保场景内有玩家(或用 MCP 在玩家可被 E001 感知的位置放一个带 Player 标签/层的对象;若测试场景无玩家系统,可退而验证 Step 5 的状态读取)。 +- `unity_play_mode`(enter) → 等一帧。 +- `unity_execute_code`(port 7890) 读取:`GameObject.Find("ENM_CaoZhi").GetComponent().CurrentStateName`。 + - 无玩家在感知范围时应为 `"Patrol"`(证明 AiRuntime 在 Tick、状态机在跑)。 +- 让玩家进入 E001 感知范围(移动玩家 Transform 到近处),再读 `CurrentStateName` 应变为 `"Chase"`;移开并等待 `LoseTimeout` 后应变 `"Search"`。 +- `unity_play_mode`(exit)。 + +- [ ] **Step 5: 记录验证结论** + +在计划文件或提交信息里记录:脚手架一键产出的 E001 由 BrainGraph 驱动、无 BehaviorTree 组件、状态随感知推进。若无玩家系统导致 Chase/Search 未能触发,至少确认 Patrol 态在跑 + 无 BehaviorTree + 注册表解析 E001 成功,并记录"完整行为待真实关卡验证"。 + +- [ ] **Step 6: 运行项目自检(合规)** + +用 MCP `unity_execute_menu_item` 依次跑(若菜单存在): +- `BaseGames/Tools/Validation/Validate All ScriptableObjects` +- `BaseGames/Addressables/Validate Address Keys` +- `BaseGames/Tools/Maintenance/Physics2D Layer Matrix/Check` +记录无新增错误。 + +- [ ] **Step 7: 提交验证产物(若有)/收尾** + +```bash +# 若 Step 1 新建了测试场景且需要保留,按规范命名保存;否则不提交场景。 +git add -- <仅本任务需保留的文件> +git commit -m "test(ai): 集成验证——脚手架一键 E001 跑在 BrainGraph 上、无 BehaviorTree" +``` + +> ⚠️ 只提交本任务确需保留的文件;不要 `git add -A`(仓库有既存无关改动)。 + +--- + +## 后续(不属本阶段) + +- LOD/错峰 tick(AiScheduler)、能力编排 sugar、参数 SO 热调(把 E001 字面量迁到 SO)。 +- 其余敌人(E002-E006)与 Boss(ChaoFeng,多阶段/加权技能/弱点)迁移;`CharacterWizardWindow` 若要显式选 AI 定义再加下拉。 +- 全部敌人达到 parity 后:删 49 个 `BD_*`、`BaseGames.Enemies.AI` 去 Opsive 引用、移除 `com.opsive.*` 包与 `GRAPH_DESIGNER` define。 +- 交付面:MermaidExporter / AiDebugService / 运行时调试器窗口。 + +--- + +## 自检记录(writing-plans self-review) + +- **Spec 覆盖**:注册表(§7.1)、上下文适配(§4/单类实现,用户已选)、EnemyAiBrain 组件(§7.1)、EnemyBase 去 BT+信号+池复用重置(§7.1/§3.6/§3.8)、脚手架接线(§7.3/§2)、E001 试点(§8)、集成验证(§11.1)。LOD/编排/参数SO/其余敌人/删BD 明确列入后续。 +- **占位扫描**:无 TBD。ContactRange 常量未用于转换已注明用途;ability 相关按"接触伤害"简化,注明若需攻击能力再加。 +- **类型一致性**:`AiDefinitionRegistry.GetGraph/Has/Ids`、`EnemyBrainContext`(实现 IAiContext+4 接口)、`EnemyAiBrain.CurrentStateName/ResetBrain/Send/DefinitionId`、`AiRuntime.Reset/Send/CurrentStateName/IsSuspended`、`AiScript.GetOrBuildGraph`、`[AiDefinition]` 跨任务一致。 +- **风险点**:Task 5(EnemyBase 手术)最需谨慎——已要求先 Read 核对真实行号、逐块刷新验证;Task 2 的 EnemyBase 成员名以真实签名为准(要求核对不臆造);Task 7 依赖测试场景有玩家,已给降级验证路径。