docs(enemy): Boss AI 双轨统一实施计划——21 个任务,分两阶段

阶段一 Task 1-15 只加不删(每 Task 编译通过、测试全绿,末尾嘲风能用新轨打起来);
阶段二 Task 16-21 删旧轨(先改引用方,再删被引用类型;先删资产,再删脚本)。

编写期核查坐实并更正 spec 两处:
- 取消 EnemyBase.SetInvincible(float):HurtBox.SetInvincible(bool) 已存在,
  玩家侧 EnableIFrame/DisableIFrame 就走它,无敌时长由动画上两事件间距表达。
  新增件由六件减为五件。
- 竞技场锚点改挂 IBossControl,不走黑板:EnemyAiBrain._context 是私有字段
  且无公开访问器(BossBase 无从写入),且 ResetScratch 会清黑板带来隐式时序约束。

另确认 EditorScaffoldUtils.CreateSOAsset(Type,...) 重载已存在,无需新增辅助。
This commit is contained in:
2026-07-30 11:01:46 +08:00
parent c176423103
commit 5839b423ae
2 changed files with 2882 additions and 8 deletions
File diff suppressed because it is too large Load Diff
@@ -193,16 +193,21 @@ public sealed class ChaoFengAi : AiScript
--- ---
## 5. 新增的 ## 5. 新增的
| # | 内容 | 规模 | | # | 内容 | 规模 |
|---|---|---| |---|---|---|
| 1 | `IBossControl` facet + `IAiContext.Boss` | 小接口 | | 1 | `IBossControl` facet + `IAiContext.Boss` | 小接口 |
| 2 | `BossPhaseAbilityGate` 组件 | 小组件 | | 2 | `BossPhaseAbilityGate` 组件 | 小组件 |
| 3 | `AttackSelectionMode.WeightedRandomAntiRepeat` + 选招器防重复 | 改两个文件 | | 3 | `AttackSelectionMode.WeightedRandomAntiRepeat` + 选招器防重复 | 改两个文件 |
| 4 | `EnemyAnimationEvents` 补 4 个 case | 各 23 行 | | 4 | `EnemyAnimationEvents` 补 4 个 case + 两个子系统引用字段 | 各 23 行 |
| 5 | `EnemyBase.SetInvincible(float)` | 约 6 行 | | 5 | `BossArenaAnchors` 组件 + 「移动到第 N 锚点」图片段 | 小组件 |
| 6 | `BossArenaAnchors` 组件 + 「移动到第 N 锚点」图片段 | 小组件 |
> 修订(2026-07-30,实施计划阶段):初稿列的第 5 件「`EnemyBase.SetInvincible(float)`」**已取消**。
> 核查 `PlayerAnimationEvents` 后确认玩家侧的 `EnableIFrame` / `DisableIFrame` 走的是
> `HurtBox.SetInvincible(bool)`——该 API **已存在**,无敌时长由动画时间轴上两个事件的间距表达。
> 敌人侧照同一形态接线即可,不需要在 `EnemyBase` 上新造带时长的无敌源。
> 这也更正确:时长归动画,不归代码常量。
### 5.1 `IBossControl` ### 5.1 `IBossControl`
@@ -216,10 +221,24 @@ namespace BaseGames.AI
bool IsPhaseTransitioning { get; } bool IsPhaseTransitioning { get; }
bool ResourceFull { get; } // BossResource 满值;无资源组件时恒 false bool ResourceFull { get; } // BossResource 满值;无资源组件时恒 false
void BeginPhaseTransition(int targetPhase, float invincibleDuration); void BeginPhaseTransition(int targetPhase, float invincibleDuration);
// 竞技场锚点(§5.5)。归本 facet 而非黑板:见下方修订说明。
Vector2 AnchorAt(int index);
float DistanceToAnchor(int index);
} }
} }
``` ```
> 修订(2026-07-30,实施计划阶段):锚点坐标初稿设计为经 `IAiContext.Blackboard` 传递
> `BossBase` 在 `Awake` 写入黑板键)。该方案有两处硬阻塞,已废弃:
> 1. **拿不到黑板** —— `EnemyAiBrain._context` 是私有字段且无公开访问器,`BossBase` 无从写入。
> 2. **时序坑** —— `EnemyBrainContext.ResetScratch()` 会 `_blackboard.Clear()`(对象池复用时调用),
> 写入点必须排在它之后,是个易错的隐式顺序依赖。
>
> 改为挂在 `IBossControl` 上:锚点本就是 Boss 专属知识,facet 已存在,`BossBase` 在 `Awake`
> 解析组件即可。净收益是少一个失败模式(黑板键缺失)、少一条隐式时序约束,且
> `DistanceToAnchor` 把"我离锚点多远"封在 Boss 内部,不需要给 AI 层引入新的位置概念。
`EnemyBrainContext.Boss => _boss ?? throw new InvalidOperationException(...)` `EnemyBrainContext.Boss => _boss ?? throw new InvalidOperationException(...)`
按 CLAUDE.md 第 6 条:小怪图误用它必须崩,不能返回一个空对象把问题盖住。 按 CLAUDE.md 第 6 条:小怪图误用它必须崩,不能返回一个空对象把问题盖住。
@@ -261,8 +280,11 @@ C# 单继承直接堵死。
| `PlaySFX` / `TriggerFeedback` | **音效预警**——竞品有 Boss 用两句语音区分物理招与远程招,预警的是招式**类别**而非具体招式,让玩家缩小可能性但不能确定。这是最经济的 telegraph 手法,现在写不出来 | | `PlaySFX` / `TriggerFeedback` | **音效预警**——竞品有 Boss 用两句语音区分物理招与远程招,预警的是招式**类别**而非具体招式,让玩家缩小可能性但不能确定。这是最经济的 telegraph 手法,现在写不出来 |
| `EnableIFrame` / `DisableIFrame` | 招式中途无敌:突进穿过玩家时不吃伤害、传送消失期间无敌 | | `EnableIFrame` / `DisableIFrame` | 招式中途无敌:突进穿过玩家时不吃伤害、传送消失期间无敌 |
配套 §5 第 5 项:`EnemyBase.IsInvincible` 是只读计算属性(`Dead || _deathSequenceActive`), 四个 case 的实现与 `PlayerAnimationEvents` 逐字对称:反馈两件走 `IFeedbackPlayer.TriggerPreset` /
没有带时长的无敌源,需新增 `SetInvincible(float)` 并纳入 `IsInvincible` `PlaySFXById``EnemyFeedback` 已实现该接口),无敌两件走 `HurtBox.SetInvincible(bool)`API 已存在)
`EnemyAnimationEvents` 因此需补两个子系统引用:`HurtBox _hurtBox``IFeedbackPlayer _feedback`
(后者在 `Awake``GetComponentInParent<IFeedbackPlayer>() ?? NullFeedbackPlayer.Instance` 解析,
与玩家侧同写法)。
### 5.5 `BossArenaAnchors` ### 5.5 `BossArenaAnchors`
@@ -270,6 +292,10 @@ C# 单继承直接堵死。
`IEnemyLocomotion.MoveTo(Vector2)` 能到点,但锚点集合无处安放。 `IEnemyLocomotion.MoveTo(Vector2)` 能到点,但锚点集合无处安放。
一个持有 `Transform[]` 的组件 + 一个「移动到第 N 个锚点」的图片段即可。 一个持有 `Transform[]` 的组件 + 一个「移动到第 N 个锚点」的图片段即可。
坐标经 `IBossControl.AnchorAt(int)` / `DistanceToAnchor(int)` 暴露给图(不走黑板,见 §5.1 修订)。
锚点物体放在场景里而非 SO 里——它随房间布局走。下标越界或引用为空**显式抛**,
不回退到 Boss 自身位置(那会让"锚点漏配"表现为"Boss 站着不动",是典型的下游兜底掩盖问题)。
--- ---
## 6. 退役清单 ## 6. 退役清单
@@ -469,8 +495,8 @@ C# 单继承直接堵死。
改 Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs CanUse += enabled;删 TelegraphRoutine 改 Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs CanUse += enabled;删 TelegraphRoutine
改 Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilitySO.cs + designNote;− telegraph 两字段;fileName → ABL_ 改 Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilitySO.cs + designNote;− telegraph 两字段;fileName → ABL_
改 Assets/_Game/Scripts/Enemies/AIBrain/Modules/Engagement/ApproachAttackEngagement.cs 建态逻辑提静态 改 Assets/_Game/Scripts/Enemies/AIBrain/Modules/Engagement/ApproachAttackEngagement.cs 建态逻辑提静态
改 Assets/_Game/Scripts/Animation/EnemyAnimationEvents.cs + 4 个 case 改 Assets/_Game/Scripts/Animation/EnemyAnimationEvents.cs + 4 个 case+ _hurtBox / _feedback 引用
改 Assets/_Game/Scripts/Enemies/EnemyBase.cs + SetInvincible改名 NotifyDecisionStop;− ConsumeParryEvent 改 Assets/_Game/Scripts/Enemies/EnemyBase.cs 改名 NotifyDecisionStop;− ConsumeParryEvent
改 Assets/_Game/Scripts/Enemies/Behaviors/EnemyDeathSequence.cs 调用点改名 改 Assets/_Game/Scripts/Enemies/Behaviors/EnemyDeathSequence.cs 调用点改名
改 Assets/_Game/Scripts/Combat/HurtBox.cs + 伤害倍率 改 Assets/_Game/Scripts/Combat/HurtBox.cs + 伤害倍率
改 Assets/_Game/Scripts/Enemies/Boss/BossBase.cs 瘦身约 150 行;实现 IBossControl 改 Assets/_Game/Scripts/Enemies/Boss/BossBase.cs 瘦身约 150 行;实现 IBossControl