docs(enemy): Boss AI 单轨落地——新增作者指南,清理描述已删除系统的长期文档

新增 Docs/Guides/09_BossAi_Authoring_Guide.md:
定制路径写法([AiDefinition] + AiScript)、建态原语(AiStateFragments +
BossFragments + IBossControl)、阶段=换招池、一招怎么配(EnemyAbilitySO +
EnemyAttackSO + 动画事件)、开战触发、死亡归物理层,
以已落地的 ChaoFengAi 为逐段范例,附 Boss 专属陷阱与已知缺口。
08 号指南加交叉引用。

删除两份整份描述已删除且从未实现的体系的文档:
Docs/Architecture/23_BossSkillModule.md、Docs/Design/47_BossSkillSystem.md。
删前已确认全库无 Markdown 链接残留,索引行与关联文档行改指新指南或 spec。

修订:25_CharacterArchitectureOverview §4 Boss 章节按实际代码重写;
07_EnemyModule 加过时提示、§11 改重定向;02_EventSystem 修正 Boss 事件发送方;
19_BossPatternLibrary 加废止提示(设计意图仍有效,类型名已废);
AssetFolderSpec 删旧 Boss 技能编辑器条目;三处 README/索引同步。
Docs_Dev/ 历史记录保留不动。

spec 标记为已实施,并记录实施期偏差(无 Boss 骨架、Telegraph 枚举保留、
阶段直查不走黑板、BossSkillEvent 一并删除)与八项已知未完成项,
其中 EnemyHurtState 无受击动画永久卡死为最高优先级、且与 Boss 轨无关。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 17:14:32 +08:00
co-authored by Claude Opus 5
parent cf0ac60fad
commit 6d3816d7ef
12 changed files with 654 additions and 242 deletions
+5 -1
View File
@@ -7,6 +7,10 @@
其中约 95% 只需建一个配方资产、两个下拉选一选,零代码零编译。读完本文档应该能在 5 分钟内判断
「我这个敌人该走哪条路径」,并在动手前看到已知的坑。
> **要写的是 Boss?** Boss 与小怪跑同一条轨(同一套能力、同一个选招器、同一个 `EnemyAiBrain`),
> 但一律走定制脚本路径,另有阶段门与竞技场锚点等专属件。
> 先读完本文的 §2 / §6,再看 [09_BossAi_Authoring_Guide](09_BossAi_Authoring_Guide.md)。
---
## 目录
@@ -141,7 +145,7 @@ Build(b):
### E004 蛭母 —— 定制脚本路径(Boss,多技能轮转 + 出场演出)
出场剧情 → 三技能轮转(撕咬/头槌连段/酸液远程,按距离和 CD/权重选择)→ 死亡分两阶段(挣扎前摇 → 爆体)。技能选择逻辑(按距离分层、CD 轮转)比 `ApproachAttackEngagement` 复杂得多,且需要「玩家绕后触发 Flip、但技能执行中不检测」这类与感知骨架无关的定制转换,属于"完全定制"的判据(与范式无关的独门机制)。Boss 是否要接入 `PerceptionSkeleton` 视具体实现取舍:若拆出"未发现(几乎不存在,出场即战斗)→ 交战(技能轮转视为一个大号 `Attack` 态)"两层意义不大,直接写定制脚本更直接。死亡链遵循 [§6](#6-常见陷阱) 的死亡规则,`Death_Pre`/`Death` 完全交给物理层(`EnemyDeathSequence` 演出 + `EnemyBase.PerformDeath`),AI 图里只需要一个 `Death` 终态兜住全局边。
出场剧情 → 三技能轮转(撕咬/头槌连段/酸液远程,按距离和 CD/权重选择)→ 死亡分两阶段(挣扎前摇 → 爆体)。技能选择逻辑(按距离分层、CD 轮转)比 `ApproachAttackEngagement` 复杂得多,且需要「玩家绕后触发 Flip、但技能执行中不检测」这类与感知骨架无关的定制转换,属于"完全定制"的判据(与范式无关的独门机制)。Boss 是否要接入 `PerceptionSkeleton` 视具体实现取舍:若拆出"未发现(几乎不存在,出场即战斗)→ 交战(技能轮转视为一个大号 `Attack` 态)"两层意义不大,直接写定制脚本更直接。死亡链遵循 [§6](#6-常见陷阱) 的死亡规则,`Death_Pre`/`Death` 完全交给物理层(`EnemyDeathSequence` 演出 + `EnemyBase.PerformDeath`),AI 图里只需要一个 `Death` 终态兜住全局边。Boss 图的写法(建态原语、阶段门、开战触发)见 [09_BossAi_Authoring_Guide](09_BossAi_Authoring_Guide.md),已落地的 `ChaoFengAi` 是完整范例。
### E005 肥蛭 —— 配方路径(精英怪,攻击选择器覆盖远近两招)
+424
View File
@@ -0,0 +1,424 @@
# Boss AI 作者指南
> 文件位置:`Docs/Guides/09_BossAi_Authoring_Guide.md`
> 版本:1.0 · 适用项目:zeling_v2
> 前置阅读:[08_EnemyAi_Authoring_Guide](08_EnemyAi_Authoring_Guide.md)(术语、建态原语、常见陷阱在那边讲过,本文不重复)
本文面向**给项目新增一个 Boss 的人**。核心结论只有一句:
> **Boss 与小怪跑的是同一条轨。** 同一套能力(`EnemyAbilitySO` + `EnemyAbilityBase`)、同一个选招器
> `EnemyAttackSelector`)、同一套建态原语(`AiStateFragments`)、同一个决策层组件(`EnemyAiBrain`)。
> Boss 独有的只有三样:**手写的 `AiScript` 图**、**一个 `IBossControl` 决策面(阶段 + 竞技场锚点)**、
> 以及几个旁挂的 MonoBehaviour(阶段门 / 锚点 / 开战触发)。
不存在"Boss 技能系统"这种独立体系。招式就是能力,阶段就是招池,没有第二套数据模型。
---
## 目录
1. [Boss 走定制路径](#1-boss-走定制路径)
2. [可用的建态原语](#2-可用的建态原语)
3. [阶段 = 换招池](#3-阶段--换招池)
4. [一招怎么配](#4-一招怎么配)
5. [开战怎么触发](#5-开战怎么触发)
6. [死亡不在 AI 层](#6-死亡不在-ai-层)
7. [完整范例:嘲风](#7-完整范例嘲风)
8. [Boss 专属陷阱](#8-boss-专属陷阱)
9. [创建链路与自检](#9-创建链路与自检)
---
## 1. Boss 走定制路径
小怪有三条作者路径(配方 / 配方+前置态 / 定制脚本,见 08 号指南 §1)。**Boss 一律走第三条:定制脚本。**
理由不是"Boss 更高级",而是数量与形状:Boss 只有个位数,彼此差异极大(有的浮空、有的分身、有的换竞技场),
把这些差异塞进可插拔模块的下拉里,只会产生一堆各用一次的模块类。手写图更直接、可读性更好,
且改一个 Boss 不会波及另一个。
写法:
```csharp
using BaseGames.AI;
namespace BaseGames.Enemies
{
[AiDefinition("<BossId>")] // ← id 字符串,全项目唯一
public sealed class XxxAi : AiScript
{
protected override void Build(BrainBuilder b) { /* 整张图写在这里 */ }
}
}
```
放在 `Assets/_Game/Scripts/Enemies/AIBrain/Ai/` 下。
接线:Boss 物体上的 `EnemyAiBrain` 组件,**`_definitionId` 填上面那个 id`_recipe` 留空**。
两者是二选一,`EnemyAiBrain.Awake` 做互斥校验——两个都配或都不配会 `Debug.LogError` 并禁用组件。
id 由 `AiDefinitionRegistry` 反射收集所有 `[AiDefinition]``AiScript` 子类解析,重复 id 在进入 Play 时直接抛异常。
`AiGraph` 按 id 共享缓存:同一个 `AiScript` 只建一次图,所有实例共用。
**直接后果:`Build()` 里的 lambda 绝对不能闭包捕获具体实例**(组件、Transform、Boss 引用一概不行),
一切实例数据只能经 `IAiContext x` 参数取。第 8 节还会再提一次,因为这是 Boss 图最容易踩的坑。
---
## 2. 可用的建态原语
建态原语是静态方法,不是可插拔模块——不需要 `[Serializable]`、不进 Inspector 下拉,直接调用。
### 2.1 通用原语 `AiStateFragments`
`Assets/_Game/Scripts/Enemies/AIBrain/Modules/AiStateFragments.cs`
| 方法 | 语义 | 用在哪 |
|---|---|---|
| `Locomotion(b, state, mode)` | 进入 / 每帧声明移动意图,离开时停。`mode``LocomotionMode.Idle` / `Face` / `Patrol` / `Pursue`。 | 静置等待、悬停朝向玩家、走位 |
| `Ability(b, state, abilityId)` | 每帧 `EnsureAbility`:能力被受击打断后,下一帧自动重新触发。 | "只要还在这个态就该一直在做"的持续态(冲锋、追击) |
| `AbilityOnce(b, state, abilityId)` | 只在 `OnEnter` 触发一次,不每帧重触发。 | 一次性演出(入场、变身),**用 `Ability` 会无限重播** |
| `Terminal(b, state)` | 无行为终态。 | 死亡态 |
| `LostAllZones`(字段) | 共享条件:脱离全部感知区。 | 小怪脱战边;**Boss 一般不用**(见 §8 |
### 2.2 Boss 原语 `BossFragments`
`Assets/_Game/Scripts/Enemies/AIBrain/Modules/BossFragments.cs`
| 方法 | 语义 |
|---|---|
| `PhaseTransition(b, state, targetPhase, invincibleDuration)` | 阶段过渡态:`OnEnter` 停移动 + 中断能力 + 调 `IBossControl.BeginPhaseTransition`。**刻意不带出边**——调用方必须自己挂 `.To(下一态).When(x => !x.Boss.IsPhaseTransitioning)`,忘了挂 Boss 会永久锁死。`targetPhase < 0``invincibleDuration <= 0` 在建图期抛异常。 |
| `ApproachAttack(b, approach, attack, rest)` | 一组「寻路逼近 ↔ 到射程选招攻击」。转发到 `ApproachAttackEngagement.Declare` 的**同一份实现**,所以小怪的交战模块与 Boss 图行为永远一致。返回**逼近态**的 builder,可继续挂阶段过渡等出边。 |
| `MoveToAnchor(b, state, index)` | 移动到第 `index` 个竞技场锚点(`BossArenaAnchors`),到位后由调用方挂出边。 |
| `AtAnchor(index, tolerance = 0.2f)` | 共享条件:已到达第 `index` 个锚点。配合上一条用。 |
### 2.3 Boss 决策面 `IBossControl`
`Assets/_Game/Scripts/AI/IBossControl.cs`。图里经 `x.Boss` 访问,由 `BossBase` 实现。
| 成员 | 用途 |
|---|---|
| `CurrentPhase` | 当前阶段索引(出生为 0 |
| `IsPhaseTransitioning` | 是否处于阶段过渡(无敌 + 演出)期间 |
| `ResourceFull` | Boss 资源(怒气 / 充能)是否已满。未挂资源组件时**抛异常**——图问了资源却没配组件是配置错误,不静默返回 false |
| `BeginPhaseTransition(targetPhase, invincibleDuration)` | 发起阶段过渡。过渡中重复调用会被忽略并告警,所以只能放 `OnEnter`,不能放 `Tick` |
| `AnchorAt(index)` / `DistanceToAnchor(index)` | 竞技场锚点坐标 / 距离。漏配组件或下标越界即抛 |
**非 Boss 敌人访问 `x.Boss` 会抛异常**`EnemyBrainContext` 里显式失败),这是有意的——
在小怪图里误用 Boss 语义应当立刻炸掉,而不是拿到一个假的默认值。
---
## 3. 阶段 = 换招池
阶段不是 SO 上的一个字段,也不是选招器里的一个过滤条件。**阶段就是"哪些能力组件当前是启用的"。**
承载它的是 `BossPhaseAbilityGate``Assets/_Game/Scripts/Enemies/Boss/BossPhaseAbilityGate.cs`),
挂在 Boss 根节点上,Inspector 里配一张表:
| 列 | 含义 |
|---|---|
| `ability` | 受阶段管控的能力组件(`EnemyAbilityBase` |
| `phases` | 该能力可用的阶段索引数组;**留空 = 全阶段可用** |
`ApplyPhase(phase)` 把该阶段的启用集刷到所有登记的能力上:`ability.enabled = 该阶段是否放行`
这条链之所以成立,是因为 `EnemyAbilityBase.CanUse``enabled` 纳入了判据——
被禁用的能力自动从 `EnemyAttackSelector` 的候选里消失。**阶段门不需要碰选招器,选招器也不知道阶段的存在。**
调用时机由 `BossBase` 直接驱动(不订阅事件频道,保证与阶段切换严格同序):
- `BossBase.OnSpawn()``ApplyPhase(0)`(对象池复用时回到初始招池)
- `BossBase.EnterPhase(phase)``ApplyPhase(phase)`
**不要在 `EnemyAbilitySO` 上写阶段字段。** SO 描述的是"这一招是什么",不是"什么时候能用"。
未登记在阶段表里的能力不受阶段门影响,保持自身启用状态——入场演出、位移类能力通常就这么放着。
表里某行漏配能力组件会在 `OnValidate``Debug.LogError` 点名行号。
---
## 4. 一招怎么配
一招 = 一个 `EnemyAbilitySO` 资产 + 一个挂在 Boss 上的 `EnemyAbilityBase` 子类组件(+ 若干 `EnemyAttackSO`)。
与小怪完全一致,没有 Boss 专用的招式资产类型。
### 4.1 `EnemyAbilitySO``ABL_` 前缀)
`CreateAssetMenu: BaseGames/Enemies/Enemy Ability`
| 字段 | 说明 |
|---|---|
| `abilityId` | 全小写英文 + 下划线,图里 `x.Combat.IsAbilityRunning("...")` 按它匹配 |
| `designNote` | 设计备注,只给编辑器看,不影响运行 |
| `attackSequence` | `EnemyAttackSO[]`,一招内的多段(如三连挥扇 = 3 个) |
| `cooldown` | 冷却(秒),从**执行结束**开始计 |
| `interruptOnHurt` / `interruptOnStagger` | 中断规则;`interruptOnHurt = false` 即该招自带霸体 |
| `category` | `AbilityCategory.Attack` 的才进选招器候选 |
| `weight` / `priority` | 选招权重与优先级 |
| `rangeRadius` / `rangeOffset` | **招式自管的圆形触发范围**`category == Attack` 时必须 `> 0`,否则永远够不着(`Awake` 报错) |
| `requiresGrounded` | 选招器的**硬门**。Boss 浮空阶段 `IsGrounded` 恒为 false,空中招留默认 `true` 会永远选不中 |
| `requiresLineOfSight` | 同上,视线硬门 |
| `exclusionGroup` | 互斥组,同组能力不能同时执行 |
需要额外可配参数(动画 / 数值 / LayerMask)的招,建 `EnemyAbilitySO` 的**类型化子类**
由能力组件的 `ResolveConfig<T>()` 解析;类型不符显式报错,不做兜底。场景引用(发射点 Transform 等)留在组件上,不进 SO。
### 4.2 `EnemyAttackSO``EATK_` 前缀)
一段动作的时间轴描述。**判定 / 生成 / 音效 / 无敌帧全部挂在动画 clip 的归一化时机上,不写在代码里。**
| 字段 | 说明 |
|---|---|
| `clip` | Animancer `ClipTransition`。**留空则该段瞬间完成**(见 §8 与 §9 的已知缺口) |
| `fallbackDuration` | 无 clip 时的兜底时长 |
| `hitBoxSlot` / `hitBoxEnterT` / `hitBoxExitT` | 命中框开关的归一化时机(0–1) |
| `damageSource` | `DamageSourceSO` |
| `projectileConfig` / `projectileCount` / `spreadAngleDeg` / `projectileFireT` | 弹体招的发射参数与时机 |
| `hasPoiseWindow` / `poiseLevel` / `poiseStartT` / `poiseEndT` | 霸体窗口 |
| `lockMovement` / `postDelay` | 收招锁移动 / 后摇 |
### 4.3 动画事件
`EnemyAnimationEvents.HandleEvent(AnimationEventType, string payload)` 支持:
`EnableHitBox` / `DisableHitBox` / `EnableIFrame` / `DisableIFrame` / `SpawnProjectile` /
`RoarStart` / `RoarEnd` / `PhaseTwoStart` / `TriggerFeedback` / `PlaySFX` / `AnimationComplete`
弹体招的判定跟着 `SpawnProjectile` 生成的弹体走,**Boss 身上不留对应 HitBox**——
留一个无人引用的 HitBox 会让下一个作者误以为该招判定挂在 Boss 身上。
### 4.4 防重复选招
`EnemyAttackSelector` 新增了 `AttackSelectionMode.WeightedRandomAntiRepeat`
`WeightedRandom`,但对上一次选中的招施加权重折扣,压低连续重复。
折扣系数配在 `EnemyStatsSO.attackAntiRepeatFactor`(0–1)。Boss 招池小、重复感明显,建议开启。
---
## 5. 开战怎么触发
Boss 出生停在静置态(`LocomotionMode.Idle`),**不巡逻、不搜敌**——等玩家进场。
把它叫醒的是 `BossFightTrigger``Assets/_Game/Scripts/Enemies/Boss/BossFightTrigger.cs`),
放在 Boss 房间入口的触发区上(`Collider2D` + `isTrigger`),只触发一次。
玩家进入时它做三件事:
```
_onBossFightStarted.Raise(bossId) // EVT_BossFightStarted:全局状态机 / 后处理 / HUD
_onBossFightToggled.Raise(true) // EVT_BossFightEnded(布尔开关):血条与 BGM
_bossBrain.Send(AiSignal.Engaged) // ← 决策层从静置态进战
```
图里对应的边:
```csharp
AiStateFragments.Locomotion(b, Wait, LocomotionMode.Idle)
.To(Intro).OnEvent(AiSignal.Engaged);
```
`AiSignal` 现有两个值:`Died``Engaged`。受击 / 硬直**不进 AI 图**(见 08 号指南 §6 第 5 条)。
漏配处理:`BossFightTrigger.Awake` 逐条点名报错(触发器未勾 Is Trigger、未指定 Brain、未填 bossId、
两个频道、玩家层掩码为空),任一缺失就整体禁用组件——不做"缺谁跳过谁"的部分执行,
因为半开的战斗(有 BGM 没血条、Boss 不动)比彻底不开更难查。
---
## 6. 死亡不在 AI 层
与小怪同一条规则,原因见 08 号指南 §6 第 3 条:`EnemyBase.PerformDeath``ForceState(Dead)`
之后 `IsControllable` 永久为假,`AiRuntime.Tick` 在让位门处直接 `return`
**任何条件边都不会再被求值**。所以死亡链一条条件边都不能放。
图里只需要两行:
```csharp
AiStateFragments.Terminal(b, Death);
b.Global().To(Death).OnEvent(AiSignal.Died);
```
演出全部归物理层:`EnemyDeathSequence`(前摇演出,`_stopDecisionLayer` 勾上则调 `EnemyBase.NotifyDecisionStop()`)、
`EnemyBase.PerformDeath`(关碰撞体、对象池归还)、具体 Boss 的击败序列(如 `ChaoFengBoss` 里的 `NotifyDecisionStop()`)。
`BossBase` 在死亡时会立即中止阶段过渡(`IsPhaseTransitioning = false`),
防止该标志永久锁死影响对象池复用。
---
## 7. 完整范例:嘲风
`Assets/_Game/Scripts/Enemies/AIBrain/Ai/ChaoFengAi.cs` —— 整张图 30 行,逐段拆解。
### 7.1 态与常量
```csharp
[AiDefinition("ChaoFeng")]
public sealed class ChaoFengAi : AiScript
{
public const string Wait = "Wait", Intro = "Intro";
public const string Ground = "Ground", GroundAttack = "GroundAttack";
public const string PhaseTx = "PhaseTransition";
public const string Air = "Air", AirAttack = "AirAttack";
public const string Death = "Death";
public const string IntroAbility = "chaofeng_intro"; // 与 ABL_ChaoFeng_Intro.abilityId 一致
private const float AirPhaseHpRatio = 0.5f; // 50% 血进空中阶段
private const int AirPhaseIndex = 1;
private const float PhaseTxInvincible = 2f; // 须 ≥ 浮空上升时长 + 缓冲
```
态名用 `public const string` 而不是字面量,是为了让 EditMode 测试(`ChaoFengAiTests`)能断言状态序列
而不靠拼字符串。态名本身只是 trace 标签,不驱动动画。
### 7.2 静置 → 入场
```csharp
b.Entry(Wait);
AiStateFragments.Locomotion(b, Wait, LocomotionMode.Idle)
.To(Intro).OnEvent(AiSignal.Engaged); // ← BossFightTrigger 发的信号
AiStateFragments.AbilityOnce(b, Intro, IntroAbility)
.To(Ground).When(x => !x.Combat.IsAbilityRunning(IntroAbility), "introDone");
```
入场用 `AbilityOnce` 而非 `Ability`:入场是一次性演出,用 `Ability` 会被每帧 `Tick` 无限重播、永远退不出去。
### 7.3 阶段 0(地面):逼近 ↔ 选招
```csharp
BossFragments.ApproachAttack(b, Ground, GroundAttack, rest: Ground)
.To(PhaseTx).When(x => x.Vitals.HpBelow(AirPhaseHpRatio), "hp<50%");
```
一行建两个态:`Ground``Locomotion.Pursue` 逼近)与 `GroundAttack``Combat.UseBestAttack()` 出招)。
`rest: Ground` 指回自身——**Boss 不脱战**,玩家跑出感知区 Boss 也不回静置态。
`ApproachAttack` 返回的是**逼近态**的 builder,所以后面 `.To(PhaseTx)` 挂在 `Ground` 上:
血量掉到 50% 时从逼近态切阶段。攻击态不挂这条边,是为了"起手就打完"(见 7.6)。
选哪一招不在图里——`Combat.UseBestAttack()` 交给 `EnemyAttackSelector`
射程 / 冷却 / 权重全在各能力自己的 `EnemyAbilitySO` 上。
### 7.4 阶段过渡
```csharp
BossFragments.PhaseTransition(b, PhaseTx, AirPhaseIndex, PhaseTxInvincible)
.To(Air).When(x => !x.Boss.IsPhaseTransitioning, "txDone");
```
`PhaseTransition` 片段**故意不自带出边**,这条 `txDone` 就是它唯一的出口——**删了 Boss 会永久锁死**
`BrainBuilder.Build()` 目前不校验"非终态是否有出边",见 §8)。
过渡期间的无敌与浮空演出由 Boss 侧承担:`BossBase.BeginPhaseTransition``IsPhaseTransitioning = true`
`IsInvincible` 随之为真),走完 `invincibleDuration` 后调 `EnterPhase(1)``BossPhaseAbilityGate.ApplyPhase(1)`
→ 地面四招被禁用、风石招被放行,最后清 `IsPhaseTransitioning`,图上这条边才放行。
### 7.5 阶段 1(空中):悬停 ↔ 选招
```csharp
AiStateFragments.Locomotion(b, Air, LocomotionMode.Face)
.To(AirAttack).When(x => x.Combat.HasEligibleAttack(), "attackInRange");
b.DeclareState(AirAttack)
.OnEnter(x => { x.Locomotion.Stop(); x.Combat.UseBestAttack(); })
.OnExit (x => x.Combat.InterruptAbilities())
.To(Air).When(x => !x.Combat.IsAbilityRunning(), "attackDone");
```
空中阶段不需要寻路逼近(浮空 + 远程招),所以不用 `ApproachAttack`,改成 `Face`(原地朝向玩家)+ 手写攻击态。
攻击态的形状与 `ApproachAttack` 生成的完全一致,只是逼近换成了悬停。
**刻意没有回地面阶段的边**——阶段单向推进,血量回升也不退阶段(招池已经换了)。
### 7.6 死亡
```csharp
AiStateFragments.Terminal(b, Death);
b.Global().To(Death).OnEvent(AiSignal.Died);
```
### 7.7 三条刻意的形状(改图时别顺手抹掉)
1. **Boss 不脱战** —— 逼近态的 `rest` 指回自身。
2. **阶段单向推进** —— 空中阶段没有回地面阶段的边。
3. **起手就打完** —— 攻击态不挂脱战边、不挂阶段边,前摇中玩家跑开仍完整打完。
观感上比"起手到一半凭空收招"合理,也避免了在能力执行途中切阶段导致的状态撕裂。
这三条在源码注释里也写了一遍,因为它们看起来都像"漏挂了一条边"。
---
## 8. Boss 专属陷阱
08 号指南 §6 的七条陷阱**全部适用**,以下是 Boss 图额外的。
1. **`Build()` 的 lambda 不能闭包捕获实例。**
`AiGraph``[AiDefinition]` id 共享缓存,一张图被所有实例共用。捕获了某个具体 Boss 的组件引用,
第二个实例就会去驱动第一个实例。锚点坐标要经 `x.Boss.AnchorAt(i)` 取,不能捕获 `Transform`——
`BossFragments.MoveToAnchor` 存在的理由正是这个。
2. **`PhaseTransition` 不带出边,必须自己挂。**
`BrainBuilder.Build()` 只校验"边指向的态是否已声明",**不校验"已声明的非终态是否有出边"**。
一个没有出边的非终态会让 Boss 永久卡住且**零报错**。阶段过渡态是这类风险的头号候选。
(补上这项校验是已记录的待办,见 §9。)
3. **浮空阶段的招不能留 `requiresGrounded = true`。**
Boss 浮空时 `IsGrounded` 恒为 false,该字段是选招器的硬门,留默认值会让整个空中招池永远选不中——
表现为"Boss 悬停着什么都不做",且没有任何报错。
4. **阶段过渡的无敌时长必须覆盖演出。**
`PhaseTxInvincible` 须 ≥ 浮空上升时长 + 缓冲。配短了 Boss 会在还没升到位时就恢复可受击并进入下一阶段。
`invincibleDuration <= 0` 在建图期就抛异常(0 意味着过渡演出期间可被打断,那不是阶段过渡的语义)。
5. **`BeginPhaseTransition` 只能在 `OnEnter` 调,不能在 `Tick`。**
过渡进行中重复调用会被忽略并告警。`BossFragments.PhaseTransition` 已经把它放在 `OnEnter`
手写阶段态时别照抄成 `Tick`
6. **Boss 的脱战语义与小怪相反。**
小怪模块默认挂 `LostAllZones` 脱战边,Boss 一般不挂。别因为"小怪都这么写"就顺手加上。
7. **`x.Boss` 在非 Boss 上会抛异常。**
这是设计如此。把 Boss 原语复制到小怪图里时会立刻炸掉,而不是拿到假数据。
---
## 9. 创建链路与自检
**不要裸建**CLAUDE.md 第 2 条)。两条脚手架已经按新轨重建:
| 目的 | 入口 |
|---|---|
| 建 Boss 的 SO 资产(`EnemyStatsSO` / `EnemyAnimationConfigSO` / 各 `EnemyAbilitySO` | 菜单 `BaseGames/Data/Character Wizard`**Boss** 标签页 |
| 在场景里放 Boss 完整组件树 | 菜单 `BaseGames/Scene/Place/Boss 嘲风 (ChaoFeng)``SceneObjectPlacerTool.PlaceChaoFeng` |
| 放开战触发区 | 菜单 `BaseGames/Scene/Place/Boss 开战触发区 (嘲风)``SceneObjectPlacerTool.PlaceBossFightTrigger(bossId)` |
| 存成预制体 | 菜单 `BaseGames/Scene/Save Prefab/Boss 嘲风 (ChaoFeng)` |
放置器产出的组件树(嘲风为例):`ChaoFengBoss`(继承 `BossBase`) + `EnemyStats` + `EnemyFeedback` +
`EnemyMovement` + `GroundNavigator` + `EnemyLocomotion` + `EnemyAiBrain` + `BossPhaseAbilityGate` +
`PhysicsPerceptionSystem` + 演出组件 + `HurtBox` / `ContactDamageZone` / 各招 `HitBox` / 弹体发射点。
> `EnemyLocomotion` 是必需的:AI 图的全部移动意图靠它落地。缺了它 `AiStateFragments` 建态时 NRE、
> `AiRuntime` 构造失败,Boss 完全不动。
新增一个 Boss 时,把上述放置器复制一份改名,**不要**在场景里手工 `AddComponent` 拼——
脚手架保证了层、命名、事件频道绑定符合规范。
建完跑三项自检(CLAUDE.md 第 3 条):
```
菜单栏 → BaseGames → Tools → Validation → Validate All ScriptableObjects
菜单栏 → BaseGames → Addressables → Validate Address Keys
菜单栏 → BaseGames → Tools → Maintenance → Physics2D Layer Matrix → Check
```
### 已知缺口(写 Boss 时会碰到)
| 缺口 | 影响 |
|---|---|
| **动画 clip 尚未接入** | `EnemyAttackSO.clip` 留空时该段瞬间完成。目前 Boss 的招"能选中、能跑完、能回到逼近态",但**判定时机、伤害、手感一律未经验证**。clip 到位后必须重新校时序 |
| **`EnemyHurtState` 无受击动画时永久卡死** | `Enter``AnimConfig.Hurt` 为空时提前返回且不安排回到 `Controlled`,敌人被打一次就永久停在 `Hurt``IsControllable` 恒假、AI 永久停摆。当前美术未接入 = 大部分敌人一挨打就废。**优先级最高的已知缺陷** |
| **池化敌人复活带着上一条命的冷却** | `EnemyBase.OnSpawn` 的中断调用注释写着"重置能力冷却"但实际没重置 |
| **`BrainBuilder.Build()` 不校验"非终态有无出边"** | 见 §8 第 2 条 |
| **`EnemyBase.ReceiveParry` 无调用方** | 伤害管线不带攻击者引用,"谁被弹反谁硬直"暂时接不上。旧的 Boss 侧弹反反制已删除(它的行为是"玩家弹反任意敌人都会让 Boss 硬直",是错的),属于**有意的移除且暂无替代** |
| **能力资产没有编辑器总览视图** | 需要一个 `EnemyAbilityModule` 编辑器窗口,尚未编写 |
| **没有 `AiDefinitionValidator`** | 能力引用漏配 / 拼错目前只能在 Play Mode 里靠肉眼排查 |