Files
zeling_v2/Docs_Dev/superpowers/specs/2026-07-24-enemy-nav-approach-attack-design.md
T

216 lines
13 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 敌人寻路逼近 + 攻击选择器 设计
> 日期:2026-07-24  状态:设计(待实现)
> 关联:`2026-07-23-enemy-pathfind-obstruction-handling-design.md`(受阻处理)、
> `2026-07-21-enemy-ability-config-to-so-design.md`(能力参数入 SO)、
> `2026-07-03-enemy-ai-framework-design.md`BrainGraph HFSM)。
## 1. 背景与目标
### 1.1 现状
- **导航层**`EnemyNavAgent` / PathBerserker2d)已完整:`RequestMoveTo` 自带重寻路防抖、受阻检测、
改道到最近可达点、NavLink(跳/落/爬)委托能力执行。
- **执行层**`EnemyLocomotion`)有 `LocomotionMode.Approach``MoveTo(Vector2)`,但目前没有任何
AI 状态真正用寻路做持续逼近。
- **决策层**`PerceptionStateMachine`)的 `Chase` 现在只能挂**单个** `ChaseAbilityId`。E001 用的
`ContactChaseAbility` 是"朝玩家一侧速度直冲、撞墙即停"的 committed 冲锋,**完全不走寻路**
被中间隔着的平台/沟拦住就停。
- **感知层**`PhysicsPerceptionSystem`)槽位驱动,`EnemyBase` 把槽位翻译成 `ISensor` 语义供 AI 读。
### 1.2 目标(本设计范围)
新增一种与"冲锋(Chase)"并列的交战范式 —— **寻路逼近(Approach)+ 到攻击范围内选招攻击**
1. 敌人沿寻路路径(含跳/落/爬 NavLink)**绕过地形持续逼近玩家**,而非直线冲。
2. 进入某招的攻击范围后,**按攻击策略从"够得着且未冷却"的候选里选一个攻击**。
3. **攻击时暂停逼近**;攻击结束回到逼近,重判距离/重选招,形成 逼近 → 攻击 → 重定位 → 攻击 的闭环。
冲锋范式(`ContactChaseAbility`)保持不变,两种范式共存、按敌人选用。
### 1.3 非目标
- 不改冲锋(Chase)范式的行为。
- 不做矩形/扇形攻击范围(本期仅圆形距离判定,`PerceptionProbe` 化留作后续扩展)。
- 不做多目标/仇恨列表(沿用现有单一玩家目标)。
- 不新建敌人预制体(预制体已延后,见 memory `enemy_prefabs_deferred`)。
## 2. 职责边界(核心原则)
| 层级 | 归属组件 | 负责 |
|---|---|---|
| **敌人级交战状态感知** | `PhysicsPerceptionSystem` | 玩家是否脱战 / 进警戒 / 进追逐等**敌人级**状态(`aggro`/`alert`/`los`/`sight` 槽) |
| **招式级攻击触发范围** | **各攻击能力自身** | 本招"够不够得着"——射程内聚在招自己,与动画/HitBox/冷却/权重同处 |
| **交战决策**(逼近↔攻击、选招) | AI 状态机(`PerceptionStateMachine` + `AiScript`) | 只决定进哪个态/转换条件/何时选招;不做位移、不做选招计算 |
| **逼近位移** | `EnemyLocomotion` | 只执行寻路逼近(新增 `Pursue` |
| **选招计算** | `EnemyAttackSelector` | 过滤候选(射程+冷却+LOS+着地)→ 按 mode 选一个;纯逻辑、可单测 |
| **攻击执行** | 各攻击能力 | 自己的动画/HitBox/朝向/停移 |
> 关键:**交战入口仍由感知系统决定**(`aggro` 槽 → `InChaseZone()` → 进 Approach);
> **"某招够不够得着"下沉到招自身**(编辑内聚:改一个招只看一个组件,不再跨到中心感知配槽、不再对名字)。
## 3. 数据流
```
感知系统(aggro/los/sight 槽) ──> ISensor ──> PerceptionStateMachine
│(是否脱战/警戒/追逐) │决策
▼ ▼
InChaseZone()=true ──> Approach态 Attack态
│Tick │OnEnter
Locomotion.Pursue(LastKnown) Combat.UseBestAttack()
│(寻路,RunSpeed) │
▼ ▼
EnemyBase.MoveTo ─> Nav(防抖/受阻/NavLink) EnemyAttackSelector
(各候选自答 InAttackRange)
选中招.Execute()(自驱动画/HitBox/停移)
```
- **Approach → Attack**`Combat.HasEligibleAttack()`(有招够得着且未冷却)。
- **Attack → Approach**`!Combat.IsAbilityRunning()`(打完回逼近重判)。
- **任意态 → Rest**:脱离全部感知区(`!InChaseZone && !InVisionZone`)。
## 4. 组件设计
### 4.1 `EnemyAbilitySO` 新增字段(选招/射程元数据,基类通用)
```csharp
[Header("调度分类与选招")]
public AbilityCategory category = AbilityCategory.None; // None/Attack/Movement/Utility
[Min(0f)] public float weight = 1f; // 权重随机用
[Header("攻击触发范围(招式自管,圆形)")]
[Min(0f)] public float rangeRadius = 0f; // 0 = 未配置(category==Attack 时 Awake 报错)
public Vector2 rangeOffset = Vector2.zero; // 相对敌人,X 随朝向翻转
// 复用现有:requiresLineOfSight / requiresGrounded / priority / cooldown
```
- `AbilityCategory` 新增枚举(`BaseGames.Enemies.Abilities`)。选择器只收 `category == Attack`
- 旧的 `preferredMinRange/preferredMaxRange` 降级为纯编辑器提示(不再参与选招),后续可删。
### 4.2 攻击能力:自答"够不够得着"
`EnemyAbilityBase` 增加:
```csharp
/// <summary>本招攻击触发范围内是否有玩家(圆形距离判定,招式自管其射程)。</summary>
public virtual bool InAttackRange()
{
if (_config == null || _config.rangeRadius <= 0f || _enemy?.PlayerTransform == null) return false;
float sign = _transform.localScale.x < 0f ? -1f : 1f;
Vector2 origin = (Vector2)_transform.position
+ new Vector2(_config.rangeOffset.x * sign, _config.rangeOffset.y);
float r = _config.rangeRadius;
return ((Vector2)_enemy.PlayerTransform.position - origin).sqrMagnitude <= r * r;
}
```
- LOS 复用敌人级 `EnemyBase.IsPlayerVisible()`(不在招上单独打射线)。
- 无物理 Overlap(有玩家 Transform,距离判定即可),零 GC、零额外物理查询。
### 4.3 `EnemyAttackSelector``EnemyBase` 持有,类比 `EnemyAbilityRegistry`
- **收集**`EnemyBase.Awake``Abilities.CollectFrom` 后,从 `Abilities.All``Config.category == Attack`
的能力为候选。
- **可单测抽象**:选择器作用于候选接口,不直连 MonoBehaviour
```csharp
public interface IAttackCandidate {
bool CanUse { get; } // 未冷却+未运行+存活
bool InAttackRange(); // 招式自管射程
bool RequiresGrounded { get; }
float Weight { get; }
int Priority { get; }
bool Execute();
}
```
`EnemyAbilityBase`(攻击类)实现之。
- **API**
```csharp
bool HasEligible(bool hasLOS, bool grounded);
IAttackCandidate Select(bool hasLOS, bool grounded, AttackSelectionMode mode); // 无合格→null
```
- **单招合格判定**`c.CanUse && c.InAttackRange() && (!requiresLOS(此招) || hasLOS)
&& (!c.RequiresGrounded || grounded)`。
- `requiresLineOfSight` 读各招 `Config``hasLOS = enemy.IsPlayerVisible()``grounded` 由 `EnemyBase` 提供。
- **选招(AttackSelectionMode**
- `WeightedRandom`:合格集内按 `Weight` 加权随机(`UnityEngine.Random`)。
- `Priority`:取 `Priority` 最高;并列取候选列表首个(确定性)。
- **根因校验**:收集后若某 `category==Attack` 候选 `rangeRadius<=0` → `LogError`(永远够不着的静默 bug 暴露);
若敌人配了 ApproachAttack 但候选集为空 → `LogError`。
### 4.4 `EnemyStatsSO` 新增
```csharp
public AttackSelectionMode attackSelectionMode = AttackSelectionMode.WeightedRandom;
```
`AttackSelectionMode { WeightedRandom, Priority }``BaseGames.Enemies` 或 `.Abilities`)。
### 4.5 `ICombatant` 扩展(`EnemyBrainContext` 实现)
```csharp
bool HasEligibleAttack(); // = selector.HasEligible(enemy.IsPlayerVisible(), enemy.Movement.IsGrounded)
bool UseBestAttack(); // sel=selector.Select(...); return sel != null && sel.Execute();
```
> 着地读 `EnemyBase.Movement.IsGrounded``EnemyMovement` 已暴露);如需可在 `EnemyBase` 加一层便捷只读属性。
- 动态量(LOS/着地/选招模式)在 `EnemyBrainContext` 侧采集后委托选择器,选择器保持无敌人耦合。
### 4.6 `IEnemyLocomotion` 扩展
```csharp
void Pursue(Vector2 target); // 寻路逼近原语:设 Approach 模式 + RunSpeed + 每帧 Nav.RequestMoveTo(target)
```
- Vector2 驱动(不耦合 Transform),底层已防抖/受阻/NavLink。
- 现有 `Approach(Transform)` 可委托 `Pursue(target.position)`。
### 4.7 `PerceptionStateMachine`:新增 ApproachAttack 交战风格(opt-in
`Config` 新增:
```csharp
public EngagementStyle Engagement = EngagementStyle.ChaseAbility; // ChaseAbility(现状) | ApproachAttack(新)
public string ApproachState = "Approach";
public string AttackState = "Attack";
```
`EngagementStyle == ApproachAttack` 时,把原单一 Chase 能力节点换成两态子机(`ChaseAbility` 风格拓扑不变):
- **Idle/Patrol/Alert** → `Approach` When `canChase`(沿用现有 `InChaseZone` / 冷却门条件)
- **Approach**Locomotion 驱动)
- `OnEnter/Tick``Locomotion.Pursue(Sensor.LastKnown)`
- `OnExit``Locomotion.Stop()`
- → `Attack` When `Combat.HasEligibleAttack()`
- → `Rest` When `!InChaseZone && !InVisionZone`
- **Attack**(能力驱动,无固定 id
- `OnEnter``Locomotion.Stop()` + `Combat.UseBestAttack()`(攻击时暂停逼近)
- → `Approach` When `!Combat.IsAbilityRunning()`(打完回逼近重判/重选招)
- → `Rest` When `!InChaseZone && !InVisionZone`(打完且脱离感知区)
- **Rest** → `Patrol`(不变)
- 全局 `Died` → `Death`(不变)
## 5. 边界与错误处理
| 情形 | 行为 |
|---|---|
| 无 NavLink 隔沟够不到玩家 | 走现有受阻逻辑 → 停在最近可达点等待(仍在 chase 区,Approach 态保持) |
| 配了 ApproachAttack 但无攻击候选 / 招 `rangeRadius<=0` | `Awake` 显式 `LogError`(根因暴露,不静默兜底,CLAUDE.md §6 |
| 玩家走出所有招射程 | 无合格攻击 → 持续逼近 |
| 攻击被受击打断 | 能力中断 → Attack.Tick 见 `!IsAbilityRunning` → 回 Approach |
| 玩家脱离感知区 | `!InChaseZone && !InVisionZone` → Rest → Patrol |
| 关闭 Domain Reload | 选择器无静态态;`UnityEngine.Random` 运行时安全(见 memory `domain_reload_disabled` |
## 6. 测试
### 6.1 EditMode 单测(`EnemyAttackSelector`
用假候选(实现 `IAttackCandidate`+ 复用 `Assets/Tests/EditMode/AI/Fakes` 范式:
- 合格过滤:冷却中 / 射程外 / 无 LOS(招 requiresLOS 且 hasLOS=false/ 需着地但腾空 → 各自被排除。
- `Priority` 模式:多招合格取最高 `Priority`,并列取首个(确定性断言)。
- `WeightedRandom` 模式:仅从合格集选(多次采样断言从不选到不合格招);单一合格招 → 必选它(确定性)。
- 空候选 / 全不合格 → `Select` 返回 null、`HasEligible` 为 false。
### 6.2 手动 / PlayMode
- 一个 `EngagementStyle=ApproachAttack` 的测试敌人(TestRoomA 内临时搭,或改 ChaoFeng):
跨平台缺口(跳 NavLink)逼近玩家 → 到某招射程停 → 选招攻击 → 打完重定位 → 玩家逃出感知区回巡逻。
- 验证攻击射程 Gizmo 与实际触发一致。
## 7. 脚手架 / 规范合规(CLAUDE.md §2/§6
- 无新资产类型(仅 `EnemyAbilitySO`/`EnemyStatsSO` 加字段)。
- 能力/角色向导需按能力类型设 `category`(攻击类→`Attack`)与默认 `weight`/`rangeRadius`——列入实现计划。
- 攻击能力在编辑器画自身射程 Gizmo(`rangeRadius`+`rangeOffset`,随朝向翻转),设计师摆招即见。
- 自检工具(`SOValidationRunner`)加一条:`category==Attack` 必须 `rangeRadius>0`。
## 8. 涉及文件(预估)
- 改:`EnemyAbilitySO.cs`+category/weight/rangeRadius/rangeOffset)、`EnemyStatsSO.cs`+attackSelectionMode)、
`EnemyAbilityBase.cs`+InAttackRange/IAttackCandidate 实现)、`EnemyBase.cs`+选择器持有/IsGrounded 暴露)、
`EnemyBrainContext.cs`+HasEligibleAttack/UseBestAttack)、`ICombatant.cs`、`IEnemyLocomotion.cs`+Pursue)、
`EnemyLocomotion.cs`+Pursue 实现)、`PerceptionStateMachine.cs`+ApproachAttack 拓扑)。
- 新:`AbilityCategory.cs`、`AttackSelectionMode.cs`、`IAttackCandidate.cs`、`EnemyAttackSelector.cs`、
`EnemyAttackSelectorTests.cs`EditMode)。
- 编辑器:攻击能力射程 Gizmo;能力向导 category/range 设置;`SOValidationRunner` 校验项。