docs(plan): 敌人能力配置迁移到 AbilitySO 子类实现计划
This commit is contained in:
@@ -0,0 +1,743 @@
|
|||||||
|
# 敌人能力配置迁移到 AbilitySO 子类 Implementation Plan
|
||||||
|
|
||||||
|
> **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:** 把 8 个能力 MonoBehaviour 上直接 `[SerializeField]` 挂载的全部数值/资产参数(动画片段、速度、时长、次数、LayerMask 等)迁移到对应的 `EnemyAbilitySO` 子类 SO;场景对象组件引用(BodyContactDamage/HitBox/HurtBox)保留在脚本。
|
||||||
|
|
||||||
|
**Architecture:** `EnemyAbilitySO` 保持基类;为 8 个能力各建 `XxxAbilitySO : EnemyAbilitySO` 子类(放 `Abilities/Configs/`,带 `[CreateAssetMenu]`)承载其参数。`EnemyAbilityBase` 新增 `ResolveConfig<T>()`:把 `_config` 解析为期望子类型,类型不符**显式报错**(无兜底)。每个能力 MonoBehaviour 删除迁走字段、`Awake` 缓存类型化 `_cfg`、使用处改读 `_cfg.xxx`,`_cfg==null` 时安全短路。更新向导 `CharacterWizardWindow` 的批量创建入口按能力类型建正确子类。不动任何 prefab/资产(用户手动重生成)。
|
||||||
|
|
||||||
|
**Tech Stack:** Unity 2022.3 (C# 9)、Animancer(`ClipTransition`)、ScriptableObject、UnityEditor、Unity Test Framework(EditMode)、Unity MCP(端口 7890) 做编译门与结构验证。
|
||||||
|
|
||||||
|
**设计文档:** `Docs_Dev/superpowers/specs/2026-07-21-enemy-ability-config-to-so-design.md`
|
||||||
|
|
||||||
|
**全局约定:**
|
||||||
|
- **编译门**:`unity_execute_code`(port 7890) 跑 `AssetDatabase.Refresh(); CompilationPipeline.RequestScriptCompilation(); return "ok";` → Bash `sleep 9` → `unity_get_compilation_errors` severity=error 期望 count=0。**绝不**在 `unity_execute_code` 内用 `Thread.Sleep`(会冻结 Unity 主线程);跨调用等待用 Bash `sleep`。桥接卡死则 `unity_editor_ping` 等恢复;保持 Unity 窗口前台。多实例时选 `zeling_v2`。
|
||||||
|
- 分支已在 `refactor/ability-config-to-so`。每个 Task 结束提交一次(中文 + 类型前缀)。每次只 `git add` 本 Task 涉及的文件。
|
||||||
|
- **不修改任何 prefab / `ABL_*.asset`**(用户后续手动重生成替换)。
|
||||||
|
- 字段命名:SO 内公有字段去掉下划线前缀(对齐 `EnemyAbilitySO`/`EnemyAttackSO` 公有字段风格),保留原 `[Header]/[Tooltip]/[Min]/[Range]` 语义。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键事实(实现前必读,均已核实)
|
||||||
|
|
||||||
|
- `EnemyAbilityBase`(`Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs`):`[SerializeField] protected EnemyAbilitySO _config;`(第 22 行);`protected virtual void Awake()`(第 46 行,做依赖缓存);`protected EnemyBase _enemy; AnimancerComponent _animancer; Transform _transform;`。子类 MonoBehaviour 可访问 `_config`(protected)。
|
||||||
|
- `EnemyAbilitySO`(同目录):公有字段 `abilityId, attackSequence(EnemyAttackSO[]), cooldown, telegraphVfxKey, telegraphDuration, interruptOnHurt, interruptOnStagger, …, exclusionGroup, priority`。带 `[CreateAssetMenu(menuName="BaseGames/Enemies/Enemy Ability", fileName="EAB_")]`。
|
||||||
|
- `ClipTransition` 是 Animancer 结构体,可作 public 字段序列化到 SO;`clip.Clip` 取底层 `AnimationClip`。
|
||||||
|
- 部分能力读 `_config?.attackSequence?[0].damageSource`(RepeatSlam/CeilingHangStrike/MeleeVulnerability)——这是**基类字段**,迁移后 `_config` 仍指向子类实例(is-a EnemyAbilitySO),这些行**保持不变**。
|
||||||
|
- EditMode 测试程序集 `Assets/Tests/EditMode/BaseGames.Tests.EditMode.asmdef` 已引用 `BaseGames.Enemies`,可直接测试能力类型。
|
||||||
|
- 能力→子类 SO 映射(供 Task 8 向导用,来自 `SceneObjectPlacerTool` 组件挂载):
|
||||||
|
- `ContactChaseAbility` → `ContactChaseAbilitySO`(E001 Chase、E006 Chase)
|
||||||
|
- `CeilingHangStrikeAbility` → `CeilingHangStrikeAbilitySO`(E002 CeilingStrike)
|
||||||
|
- `AnimatedCeilingDropAbility` → `AnimatedCeilingDropAbilitySO`(E003 Fall)
|
||||||
|
- `AppearAbility` → `AppearAbilitySO`(E004 Appear)
|
||||||
|
- `RepeatSlamAbility` → `RepeatSlamAbilitySO`(E004 HeadSlam)
|
||||||
|
- `FacePlayerAbility` → `FacePlayerAbilitySO`(E004 Flip)
|
||||||
|
- `MeleeAttackAbility`/`ProjectileAttackAbility`/`LeapAttackAbility` → 基类 `EnemyAbilitySO`(attackSequence 型,不迁移;E004/E005 Bite、Acid、E006 Leap)
|
||||||
|
- E001 Alert(e001_alert)→ 基类 `EnemyAbilitySO`(无对应迁移能力)
|
||||||
|
- `MeleeVulnerabilityAbility`/`PlayClipAbility` → 各自子类 SO(框架能力,当前无向导 def 映射,仅 CreateAssetMenu)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: 基类 `ResolveConfig<T>` + EditMode 测试
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs`
|
||||||
|
- Create: `Assets/Tests/EditMode/Abilities/AbilityConfigResolveTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败测试**
|
||||||
|
|
||||||
|
创建 `Assets/Tests/EditMode/Abilities/AbilityConfigResolveTests.cs`:
|
||||||
|
```csharp
|
||||||
|
using System.Collections;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.TestTools;
|
||||||
|
using BaseGames.Enemies.Abilities;
|
||||||
|
|
||||||
|
namespace BaseGames.Tests.EditMode.Abilities
|
||||||
|
{
|
||||||
|
public class AbilityConfigResolveTests
|
||||||
|
{
|
||||||
|
// 测试专用 SO 子类型(不依赖真实 8 个子类,保持本 Task 自包含)
|
||||||
|
private sealed class ProbeConfigSO : EnemyAbilitySO { }
|
||||||
|
private sealed class OtherConfigSO : EnemyAbilitySO { }
|
||||||
|
|
||||||
|
// 测试探针:跳过基类 Awake 依赖装配,暴露 protected 成员
|
||||||
|
private sealed class ProbeAbility : EnemyAbilityBase
|
||||||
|
{
|
||||||
|
protected override void Awake() { /* 跳过依赖装配,避免测试噪声 */ }
|
||||||
|
protected override IEnumerator ExecuteCoroutine() { yield break; }
|
||||||
|
public void SetConfig(EnemyAbilitySO c) => _config = c;
|
||||||
|
public T Probe<T>() where T : EnemyAbilitySO => ResolveConfig<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProbeAbility Make() => new GameObject("probe").AddComponent<ProbeAbility>();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ResolveConfig_CorrectType_ReturnsInstance()
|
||||||
|
{
|
||||||
|
var probe = Make();
|
||||||
|
var cfg = ScriptableObject.CreateInstance<ProbeConfigSO>();
|
||||||
|
probe.SetConfig(cfg);
|
||||||
|
Assert.AreSame(cfg, probe.Probe<ProbeConfigSO>());
|
||||||
|
Object.DestroyImmediate(cfg);
|
||||||
|
Object.DestroyImmediate(probe.gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ResolveConfig_WrongType_LogsError_ReturnsNull()
|
||||||
|
{
|
||||||
|
var probe = Make();
|
||||||
|
probe.SetConfig(ScriptableObject.CreateInstance<OtherConfigSO>());
|
||||||
|
LogAssert.Expect(LogType.Error, new Regex("_config 需为"));
|
||||||
|
Assert.IsNull(probe.Probe<ProbeConfigSO>());
|
||||||
|
Object.DestroyImmediate(probe.gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ResolveConfig_Null_LogsError_ReturnsNull()
|
||||||
|
{
|
||||||
|
var probe = Make();
|
||||||
|
probe.SetConfig(null);
|
||||||
|
LogAssert.Expect(LogType.Error, new Regex("_config 需为"));
|
||||||
|
Assert.IsNull(probe.Probe<ProbeConfigSO>());
|
||||||
|
Object.DestroyImmediate(probe.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 编译门确认测试因缺 `ResolveConfig` 编译失败** → `unity_get_compilation_errors` 应报 `ResolveConfig` 不存在(预期红)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 在 `EnemyAbilityBase` 加 `ResolveConfig<T>`**
|
||||||
|
|
||||||
|
在 `FaceTarget` 方法之后(类内、`EnemyAbilityWaits` 之前)插入:
|
||||||
|
```csharp
|
||||||
|
/// <summary>把 _config 解析为期望的子类型;类型不符/为空时显式报错(不回退)。</summary>
|
||||||
|
protected T ResolveConfig<T>() where T : EnemyAbilitySO
|
||||||
|
{
|
||||||
|
if (_config is T typed) return typed;
|
||||||
|
Debug.LogError($"[{GetType().Name}] _config 需为 {typeof(T).Name}," +
|
||||||
|
$"实际 = {(_config != null ? _config.GetType().Name : "null")}。请重建为正确的子类 SO。", this);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 编译门** → count=0。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 跑 EditMode 测试**(Unity Test Runner)
|
||||||
|
|
||||||
|
`unity_execute_code`(port 7890) 通过反射跑测试类(或用编辑器 Test Runner)。若用反射直跑:调用三个 `[Test]` 方法,捕获 `AssertionException`;`LogAssert.Expect` 需在 Test Runner 环境内生效,若反射直跑不便,改为在编辑器 `Window > General > Test Runner > EditMode > Run` 手动确认 3/3 通过(在报告中注明验证方式)。期望:3 通过。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交**
|
||||||
|
```bash
|
||||||
|
git add Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs Assets/Tests/EditMode/Abilities/AbilityConfigResolveTests.cs
|
||||||
|
git commit -m "feat(enemy): EnemyAbilityBase 加 ResolveConfig<T> 类型化配置解析(不匹配显式报错) + EditMode 测试"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: ContactChaseAbilitySO + 迁移 ContactChaseAbility(参考样板)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `Assets/_Game/Scripts/Enemies/Abilities/Configs/ContactChaseAbilitySO.cs`
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/Abilities/ContactChaseAbility.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 建子类 SO**
|
||||||
|
```csharp
|
||||||
|
using Animancer;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Enemies.Abilities
|
||||||
|
{
|
||||||
|
/// <summary>ContactChaseAbility 的配置:起手/循环/收招动画 + 冲刺速度。</summary>
|
||||||
|
[CreateAssetMenu(menuName = "BaseGames/Enemies/Abilities/Contact Chase Ability", fileName = "ABL_")]
|
||||||
|
public class ContactChaseAbilitySO : EnemyAbilitySO
|
||||||
|
{
|
||||||
|
[Header("动画(起手 / 循环 / 收招)")]
|
||||||
|
public ClipTransition startClip; // Skill_Start:张口(一次性)
|
||||||
|
public ClipTransition loopClip; // Skill_Loop:快速爬行追击(循环)
|
||||||
|
public ClipTransition endClip; // Skill_End:收招(一次性)
|
||||||
|
|
||||||
|
[Header("冲刺")]
|
||||||
|
[Tooltip("冲刺速度;<=0 时回退用 EnemyStatsSO.RunSpeed(追击速度)")]
|
||||||
|
public float dashSpeed = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 迁移 MonoBehaviour**
|
||||||
|
|
||||||
|
先 Read `ContactChaseAbility.cs` 全文。做以下改动:
|
||||||
|
|
||||||
|
(a) 删除字段块(第 16-19 行 `_startClip/_loopClip/_endClip` 与第 24-26 行 `_dashSpeed`),**保留** `[Header("接触伤害")] _contactDamage`。删除后字段区变为:
|
||||||
|
```csharp
|
||||||
|
[Header("接触伤害")]
|
||||||
|
[SerializeField] private BodyContactDamage _contactDamage;
|
||||||
|
|
||||||
|
private ContactChaseAbilitySO _cfg;
|
||||||
|
|
||||||
|
protected override void Awake()
|
||||||
|
{
|
||||||
|
base.Awake();
|
||||||
|
_cfg = ResolveConfig<ContactChaseAbilitySO>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(b) 使用处替换(`ExecuteCoroutine` 顶部加空配置守卫):
|
||||||
|
- `ExecuteCoroutine` 第一行 `Phase = AbilityRunState.Active;` 前加:`if (_cfg == null) yield break;`
|
||||||
|
- `_startClip.Clip` → `_cfg.startClip.Clip`(两处:判空 + `LockAnim(_startClip.Clip.length)` + `_animancer.Play(_startClip)` + `EnemyAbilityWaits.Get(_startClip.Clip.length)`)
|
||||||
|
- `_dashSpeed` → `_cfg.dashSpeed`
|
||||||
|
- `_loopClip.Clip`/`_animancer.Play(_loopClip)` → `_cfg.loopClip.Clip`/`_animancer.Play(_cfg.loopClip)`
|
||||||
|
- `_endClip` 全部(`OnInterrupted`、`PlaySkillEnd`)→ `_cfg.endClip`;`OnInterrupted` 内条件改为 `if (reason == InterruptReason.ExternalRequest && _cfg != null && _cfg.endClip.Clip != null)`;`PlaySkillEnd` 首行改为 `if (_cfg == null || _cfg.endClip.Clip == null) yield break;`
|
||||||
|
|
||||||
|
> 场景引用 `_contactDamage` 及其 `DisableContact()/Cleanup()` 逻辑不动。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 编译门** → count=0。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交**
|
||||||
|
```bash
|
||||||
|
git add Assets/_Game/Scripts/Enemies/Abilities/Configs/ContactChaseAbilitySO.cs Assets/_Game/Scripts/Enemies/Abilities/ContactChaseAbility.cs
|
||||||
|
git commit -m "refactor(enemy): ContactChase 参数迁入 ContactChaseAbilitySO(子类)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: RepeatSlamAbilitySO + 迁移 RepeatSlamAbility
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `Assets/_Game/Scripts/Enemies/Abilities/Configs/RepeatSlamAbilitySO.cs`
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/Abilities/RepeatSlamAbility.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 建子类 SO**
|
||||||
|
```csharp
|
||||||
|
using Animancer;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Enemies.Abilities
|
||||||
|
{
|
||||||
|
/// <summary>RepeatSlamAbility 的配置:起手/循环/收招动画 + 砸地行为参数。</summary>
|
||||||
|
[CreateAssetMenu(menuName = "BaseGames/Enemies/Abilities/Repeat Slam Ability", fileName = "ABL_")]
|
||||||
|
public class RepeatSlamAbilitySO : EnemyAbilitySO
|
||||||
|
{
|
||||||
|
[Header("动画")]
|
||||||
|
public ClipTransition startClip;
|
||||||
|
public ClipTransition loopClip;
|
||||||
|
public ClipTransition endClip;
|
||||||
|
|
||||||
|
[Header("行为配置")]
|
||||||
|
[Tooltip("每次砸地时 HitBox 激活时长(秒)")]
|
||||||
|
public float hitActiveTime = 0.15f;
|
||||||
|
[Tooltip("砸地次数")]
|
||||||
|
public int slamCount = 2;
|
||||||
|
[Tooltip("最后一次砸地收招后额外等待时间(秒)")]
|
||||||
|
public float staggerDuration = 1.2f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 迁移 MonoBehaviour**
|
||||||
|
|
||||||
|
Read `RepeatSlamAbility.cs` 全文。改动:
|
||||||
|
|
||||||
|
(a) 删除第 15-18 行动画字段块与第 23-29 行 `_hitActiveTime/_slamCount/_staggerDuration`,**保留** `[Header("打击")] HitBox _hitBox`。字段区改为:
|
||||||
|
```csharp
|
||||||
|
[Header("打击")]
|
||||||
|
[SerializeField] private HitBox _hitBox;
|
||||||
|
|
||||||
|
private RepeatSlamAbilitySO _cfg;
|
||||||
|
|
||||||
|
protected override void Awake()
|
||||||
|
{
|
||||||
|
base.Awake();
|
||||||
|
_cfg = ResolveConfig<RepeatSlamAbilitySO>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(b) 使用处替换:
|
||||||
|
- `ExecuteCoroutine` 顶 `Phase = AbilityRunState.Active;` 前加 `if (_cfg == null) yield break;`
|
||||||
|
- `_startClip` → `_cfg.startClip`(判空 + Play + `Get(_startClip.Clip.length)`)
|
||||||
|
- `_slamCount` → `_cfg.slamCount`
|
||||||
|
- `_loopClip` → `_cfg.loopClip`;`_hitActiveTime` → `_cfg.hitActiveTime`(第 46、52 行)
|
||||||
|
- `_endClip` → `_cfg.endClip`;`_staggerDuration` → `_cfg.staggerDuration`(第 59-66 行两处)
|
||||||
|
- `_config?.attackSequence?.Length > 0 ? _config.attackSequence[0].damageSource : null` **保持不变**(基类字段)。
|
||||||
|
- `OnInterrupted` 的 `_hitBox?.Deactivate();` 不动。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 编译门** → count=0。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交**
|
||||||
|
```bash
|
||||||
|
git add Assets/_Game/Scripts/Enemies/Abilities/Configs/RepeatSlamAbilitySO.cs Assets/_Game/Scripts/Enemies/Abilities/RepeatSlamAbility.cs
|
||||||
|
git commit -m "refactor(enemy): RepeatSlam 参数迁入 RepeatSlamAbilitySO(子类)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: CeilingHangStrikeAbilitySO + 迁移 CeilingHangStrikeAbility
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `Assets/_Game/Scripts/Enemies/Abilities/Configs/CeilingHangStrikeAbilitySO.cs`
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/Abilities/CeilingHangStrikeAbility.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 建子类 SO**
|
||||||
|
```csharp
|
||||||
|
using Animancer;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Enemies.Abilities
|
||||||
|
{
|
||||||
|
/// <summary>CeilingHangStrikeAbility 的配置:出击/悬挂/收回动画 + 悬挂窗口时长。</summary>
|
||||||
|
[CreateAssetMenu(menuName = "BaseGames/Enemies/Abilities/Ceiling Hang Strike Ability", fileName = "ABL_")]
|
||||||
|
public class CeilingHangStrikeAbilitySO : EnemyAbilitySO
|
||||||
|
{
|
||||||
|
[Header("动画")]
|
||||||
|
public ClipTransition strikeClip;
|
||||||
|
public ClipTransition loopClip;
|
||||||
|
public ClipTransition endClip;
|
||||||
|
|
||||||
|
[Header("行为")]
|
||||||
|
[Tooltip("脆弱悬挂窗口持续时间(秒)")]
|
||||||
|
public float hangDuration = 2f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 迁移 MonoBehaviour**
|
||||||
|
|
||||||
|
Read `CeilingHangStrikeAbility.cs`。改动:
|
||||||
|
|
||||||
|
(a) 删第 15-18 行动画字段块 + 第 24-26 行 `_hangDuration`,**保留** `[Header("碰撞")] HitBox _attackHitBox; HurtBox _hurtBox`。字段区改为:
|
||||||
|
```csharp
|
||||||
|
[Header("碰撞")]
|
||||||
|
[SerializeField] private HitBox _attackHitBox;
|
||||||
|
[SerializeField] private HurtBox _hurtBox;
|
||||||
|
|
||||||
|
private CeilingHangStrikeAbilitySO _cfg;
|
||||||
|
|
||||||
|
protected override void Awake()
|
||||||
|
{
|
||||||
|
base.Awake();
|
||||||
|
_cfg = ResolveConfig<CeilingHangStrikeAbilitySO>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(b) 使用处替换:
|
||||||
|
- `ExecuteCoroutine` 顶 `Phase = AbilityRunState.Active;` 前加 `if (_cfg == null) yield break;`
|
||||||
|
- `_strikeClip` → `_cfg.strikeClip`(判空 + Play + `Get(_strikeClip.Clip.length)`)
|
||||||
|
- `_loopClip` → `_cfg.loopClip`
|
||||||
|
- `_hangDuration` → `_cfg.hangDuration`
|
||||||
|
- `_endClip` → `_cfg.endClip`
|
||||||
|
- `_config?.attackSequence?...damageSource` **保持不变**。
|
||||||
|
- `_attackHitBox`/`_hurtBox` 相关不动。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 编译门** → count=0。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交**
|
||||||
|
```bash
|
||||||
|
git add Assets/_Game/Scripts/Enemies/Abilities/Configs/CeilingHangStrikeAbilitySO.cs Assets/_Game/Scripts/Enemies/Abilities/CeilingHangStrikeAbility.cs
|
||||||
|
git commit -m "refactor(enemy): CeilingHangStrike 参数迁入 CeilingHangStrikeAbilitySO(子类)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: AnimatedCeilingDropAbilitySO + 迁移 AnimatedCeilingDropAbility
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `Assets/_Game/Scripts/Enemies/Abilities/Configs/AnimatedCeilingDropAbilitySO.cs`
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/Abilities/AnimatedCeilingDropAbility.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 建子类 SO**
|
||||||
|
```csharp
|
||||||
|
using Animancer;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Enemies.Abilities
|
||||||
|
{
|
||||||
|
/// <summary>AnimatedCeilingDropAbility 的配置:下落动画 + 物理下落/落地参数。</summary>
|
||||||
|
[CreateAssetMenu(menuName = "BaseGames/Enemies/Abilities/Animated Ceiling Drop Ability", fileName = "ABL_")]
|
||||||
|
public class AnimatedCeilingDropAbilitySO : EnemyAbilitySO
|
||||||
|
{
|
||||||
|
[Header("动画")]
|
||||||
|
[Tooltip("下落循环动画(旋转下落姿态,循环播放直到落地)")]
|
||||||
|
public ClipTransition fallLoopClip;
|
||||||
|
|
||||||
|
[Header("物理下落")]
|
||||||
|
[Tooltip("切换 Dynamic 后使用的重力倍率")]
|
||||||
|
public float fallGravityScale = 3.5f;
|
||||||
|
[Tooltip("下落超时保护(秒),超时后强制继续执行")]
|
||||||
|
public float maxFallTime = 3f;
|
||||||
|
public LayerMask groundMask;
|
||||||
|
|
||||||
|
[Header("落地后")]
|
||||||
|
[Tooltip("落地后恢复帧延迟(秒),之后切换为 Patrol 阶段")]
|
||||||
|
public float recoveryTime = 0.1f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 迁移 MonoBehaviour**
|
||||||
|
|
||||||
|
Read `AnimatedCeilingDropAbility.cs`。注意本类**已有** `Awake` 重写(缓存 `_rb`)。改动:
|
||||||
|
|
||||||
|
(a) 删第 16-29 行的动画/物理/落地数值字段(`_fallLoopClip/_fallGravityScale/_maxFallTime/_groundMask/_recoveryTime`),**保留** `[SerializeField] private BodyContactDamage _contactDamage;` 与 `private Rigidbody2D _rb;`。字段区改为:
|
||||||
|
```csharp
|
||||||
|
[Header("落地后")]
|
||||||
|
[SerializeField] private BodyContactDamage _contactDamage;
|
||||||
|
|
||||||
|
private AnimatedCeilingDropAbilitySO _cfg;
|
||||||
|
private Rigidbody2D _rb;
|
||||||
|
```
|
||||||
|
|
||||||
|
(b) 现有 `Awake` 重写内补解析(保留 `base.Awake()` 与 `_rb` 缓存):
|
||||||
|
```csharp
|
||||||
|
protected override void Awake()
|
||||||
|
{
|
||||||
|
base.Awake();
|
||||||
|
_cfg = ResolveConfig<AnimatedCeilingDropAbilitySO>();
|
||||||
|
_rb = GetComponentInParent<Rigidbody2D>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(c) 使用处替换:
|
||||||
|
- `ExecuteCoroutine` 顶 `Phase = AbilityRunState.Active;` 前加 `if (_cfg == null) yield break;`
|
||||||
|
- `_fallLoopClip` → `_cfg.fallLoopClip`
|
||||||
|
- `_fallGravityScale` → `_cfg.fallGravityScale`
|
||||||
|
- `_maxFallTime` → `_cfg.maxFallTime`
|
||||||
|
- `_recoveryTime` → `_cfg.recoveryTime`
|
||||||
|
- `IsGrounded()` 内 `_groundMask` → `_cfg.groundMask`(注意:`IsGrounded` 是普通方法,`_cfg` 为实例字段可访问;若 `_cfg` 可能为 null,此路径仅在 `ExecuteCoroutine` 守卫通过后调用,安全)
|
||||||
|
- `_contactDamage`、`_rb`、`OnInterrupted` 不动。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 编译门** → count=0。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交**
|
||||||
|
```bash
|
||||||
|
git add Assets/_Game/Scripts/Enemies/Abilities/Configs/AnimatedCeilingDropAbilitySO.cs Assets/_Game/Scripts/Enemies/Abilities/AnimatedCeilingDropAbility.cs
|
||||||
|
git commit -m "refactor(enemy): AnimatedCeilingDrop 参数迁入 AnimatedCeilingDropAbilitySO(子类)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: MeleeVulnerabilityAbilitySO + 迁移 MeleeVulnerabilityAbility
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `Assets/_Game/Scripts/Enemies/Abilities/Configs/MeleeVulnerabilityAbilitySO.cs`
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/Abilities/MeleeVulnerabilityAbility.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 建子类 SO**
|
||||||
|
```csharp
|
||||||
|
using Animancer;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Enemies.Abilities
|
||||||
|
{
|
||||||
|
/// <summary>MeleeVulnerabilityAbility 的配置:攻击动画 + 打击归一化时机 + 后摇脆弱时长。</summary>
|
||||||
|
[CreateAssetMenu(menuName = "BaseGames/Enemies/Abilities/Melee Vulnerability Ability", fileName = "ABL_")]
|
||||||
|
public class MeleeVulnerabilityAbilitySO : EnemyAbilitySO
|
||||||
|
{
|
||||||
|
[Header("动画")]
|
||||||
|
public ClipTransition attackClip;
|
||||||
|
|
||||||
|
[Header("打击时间(0~1 归一化,基于 Clip 时长)")]
|
||||||
|
[Range(0f, 1f)] public float hitEnterT = 0.30f;
|
||||||
|
[Range(0f, 1f)] public float hitExitT = 0.60f;
|
||||||
|
|
||||||
|
[Header("后摇脆弱窗口")]
|
||||||
|
[Tooltip("HurtBox 激活时间(秒)")]
|
||||||
|
public float staggerDuration = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 迁移 MonoBehaviour**
|
||||||
|
|
||||||
|
Read `MeleeVulnerabilityAbility.cs`。改动:
|
||||||
|
|
||||||
|
(a) 删第 15-16 行 `_attackClip`、第 22-24 行 `_hitEnterT/_hitExitT`、第 26-28 行 `_staggerDuration`,**保留** `[Header("碰撞")] HitBox _hitBox; HurtBox _hurtBox`。字段区改为:
|
||||||
|
```csharp
|
||||||
|
[Header("碰撞")]
|
||||||
|
[SerializeField] private HitBox _hitBox;
|
||||||
|
[SerializeField] private HurtBox _hurtBox;
|
||||||
|
|
||||||
|
private MeleeVulnerabilityAbilitySO _cfg;
|
||||||
|
|
||||||
|
protected override void Awake()
|
||||||
|
{
|
||||||
|
base.Awake();
|
||||||
|
_cfg = ResolveConfig<MeleeVulnerabilityAbilitySO>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(b) 使用处替换:
|
||||||
|
- `ExecuteCoroutine` 顶 `Phase = AbilityRunState.Active;` 前加 `if (_cfg == null) yield break;`
|
||||||
|
- `_attackClip` → `_cfg.attackClip`(判空 + `len = _attackClip.Clip.length` + Play)
|
||||||
|
- `_hitEnterT` → `_cfg.hitEnterT`;`_hitExitT` → `_cfg.hitExitT`
|
||||||
|
- `_staggerDuration` → `_cfg.staggerDuration`
|
||||||
|
- `_config?.attackSequence?...damageSource` **保持不变**。
|
||||||
|
- `_hitBox`/`_hurtBox`、`OnInterrupted` 不动。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 编译门** → count=0。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交**
|
||||||
|
```bash
|
||||||
|
git add Assets/_Game/Scripts/Enemies/Abilities/Configs/MeleeVulnerabilityAbilitySO.cs Assets/_Game/Scripts/Enemies/Abilities/MeleeVulnerabilityAbility.cs
|
||||||
|
git commit -m "refactor(enemy): MeleeVulnerability 参数迁入 MeleeVulnerabilityAbilitySO(子类)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: 三个单片段能力(Appear / FacePlayer / PlayClip)子类 SO + 迁移
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `Assets/_Game/Scripts/Enemies/Abilities/Configs/AppearAbilitySO.cs`
|
||||||
|
- Create: `Assets/_Game/Scripts/Enemies/Abilities/Configs/FacePlayerAbilitySO.cs`
|
||||||
|
- Create: `Assets/_Game/Scripts/Enemies/Abilities/Configs/PlayClipAbilitySO.cs`
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/Abilities/AppearAbility.cs`
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/Abilities/FacePlayerAbility.cs`
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/Abilities/PlayClipAbility.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 建三个子类 SO**
|
||||||
|
|
||||||
|
`AppearAbilitySO.cs`:
|
||||||
|
```csharp
|
||||||
|
using Animancer;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Enemies.Abilities
|
||||||
|
{
|
||||||
|
/// <summary>AppearAbility 的配置:出场动画。</summary>
|
||||||
|
[CreateAssetMenu(menuName = "BaseGames/Enemies/Abilities/Appear Ability", fileName = "ABL_")]
|
||||||
|
public class AppearAbilitySO : EnemyAbilitySO
|
||||||
|
{
|
||||||
|
public ClipTransition appearClip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`FacePlayerAbilitySO.cs`:
|
||||||
|
```csharp
|
||||||
|
using Animancer;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Enemies.Abilities
|
||||||
|
{
|
||||||
|
/// <summary>FacePlayerAbility 的配置:转身/翻转动画。</summary>
|
||||||
|
[CreateAssetMenu(menuName = "BaseGames/Enemies/Abilities/Face Player Ability", fileName = "ABL_")]
|
||||||
|
public class FacePlayerAbilitySO : EnemyAbilitySO
|
||||||
|
{
|
||||||
|
public ClipTransition faceClip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`PlayClipAbilitySO.cs`:
|
||||||
|
```csharp
|
||||||
|
using Animancer;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BaseGames.Enemies.Abilities
|
||||||
|
{
|
||||||
|
/// <summary>PlayClipAbility 的配置:待播放的单个动画 Clip。</summary>
|
||||||
|
[CreateAssetMenu(menuName = "BaseGames/Enemies/Abilities/Play Clip Ability", fileName = "ABL_")]
|
||||||
|
public class PlayClipAbilitySO : EnemyAbilitySO
|
||||||
|
{
|
||||||
|
public ClipTransition clip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 迁移 AppearAbility**
|
||||||
|
|
||||||
|
Read `AppearAbility.cs`。删 `[SerializeField] private ClipTransition _appearClip;`,加:
|
||||||
|
```csharp
|
||||||
|
private AppearAbilitySO _cfg;
|
||||||
|
|
||||||
|
protected override void Awake()
|
||||||
|
{
|
||||||
|
base.Awake();
|
||||||
|
_cfg = ResolveConfig<AppearAbilitySO>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`ExecuteCoroutine` 改为:
|
||||||
|
```csharp
|
||||||
|
protected override IEnumerator ExecuteCoroutine()
|
||||||
|
{
|
||||||
|
Phase = AbilityRunState.Active;
|
||||||
|
if (_cfg == null || _cfg.appearClip.Clip == null) yield break;
|
||||||
|
|
||||||
|
_animancer.Play(_cfg.appearClip);
|
||||||
|
yield return EnemyAbilityWaits.Get(_cfg.appearClip.Clip.length);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 迁移 FacePlayerAbility**
|
||||||
|
|
||||||
|
Read `FacePlayerAbility.cs`。删 `_faceClip`,加同款 `_cfg`(类型 `FacePlayerAbilitySO`) + `Awake`。`CanUse` 重写不动。`ExecuteCoroutine` 尾部改:
|
||||||
|
```csharp
|
||||||
|
_enemy.FacePlayer();
|
||||||
|
|
||||||
|
if (_cfg == null || _cfg.faceClip.Clip == null) yield break;
|
||||||
|
|
||||||
|
_animancer.Play(_cfg.faceClip);
|
||||||
|
yield return EnemyAbilityWaits.Get(_cfg.faceClip.Clip.length);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 迁移 PlayClipAbility**
|
||||||
|
|
||||||
|
Read `PlayClipAbility.cs`。删 `_clip`,加同款 `_cfg`(类型 `PlayClipAbilitySO`) + `Awake`。`ExecuteCoroutine` 改:
|
||||||
|
```csharp
|
||||||
|
protected override IEnumerator ExecuteCoroutine()
|
||||||
|
{
|
||||||
|
Phase = AbilityRunState.Active;
|
||||||
|
if (_cfg == null || _cfg.clip.Clip == null) yield break;
|
||||||
|
|
||||||
|
_animancer.Play(_cfg.clip);
|
||||||
|
yield return EnemyAbilityWaits.Get(_cfg.clip.Clip.length);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 编译门** → count=0。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交**
|
||||||
|
```bash
|
||||||
|
git add Assets/_Game/Scripts/Enemies/Abilities/Configs/AppearAbilitySO.cs Assets/_Game/Scripts/Enemies/Abilities/Configs/FacePlayerAbilitySO.cs Assets/_Game/Scripts/Enemies/Abilities/Configs/PlayClipAbilitySO.cs Assets/_Game/Scripts/Enemies/Abilities/AppearAbility.cs Assets/_Game/Scripts/Enemies/Abilities/FacePlayerAbility.cs Assets/_Game/Scripts/Enemies/Abilities/PlayClipAbility.cs
|
||||||
|
git commit -m "refactor(enemy): Appear/FacePlayer/PlayClip 参数迁入各自子类 SO"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: 脚手架——向导按能力类型创建正确子类 SO
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Assets/_Game/Scripts/Editor/Shared/EditorScaffoldUtils.cs`(加 Type 版 `CreateSOAsset` 重载)
|
||||||
|
- Modify: `Assets/_Game/Scripts/Editor/Character/CharacterWizardWindow.cs`(def 带类型 + 透传)
|
||||||
|
|
||||||
|
- [ ] **Step 1: `EditorScaffoldUtils` 加 Type 版 `CreateSOAsset` 重载**
|
||||||
|
|
||||||
|
Read `EditorScaffoldUtils.cs` 现有泛型 `CreateSOAsset<T>`(约 154 行)。在其后加非泛型重载(复用同样的存在性检查/建目录/保存逻辑):
|
||||||
|
```csharp
|
||||||
|
/// <summary>按运行时 Type 创建 SO 资产(用于需要动态子类型的场景,如能力子类 SO)。</summary>
|
||||||
|
public static ScriptableObject CreateSOAsset(System.Type soType, string folder, string assetName)
|
||||||
|
{
|
||||||
|
string dir = folder.TrimEnd('/');
|
||||||
|
string path = $"{dir}/{assetName}.asset";
|
||||||
|
|
||||||
|
if (AssetDatabase.LoadAssetAtPath<ScriptableObject>(path) != null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[EditorScaffoldUtils] 资产已存在,跳过创建:{path}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureFolder(dir);
|
||||||
|
var asset = ScriptableObject.CreateInstance(soType);
|
||||||
|
AssetDatabase.CreateAsset(asset, path);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> 若现有泛型版有额外步骤(如 Selection/Ping),读后与之保持一致;此重载只需产出资产。
|
||||||
|
|
||||||
|
- [ ] **Step 2: `GetEnemyAbilityDefs` 加 SO 类型**
|
||||||
|
|
||||||
|
在 `CharacterWizardWindow.cs`,把 `GetEnemyAbilityDefs` 的返回元组从 `(string ablName, string ablId)` 扩为 `(string ablName, string ablId, System.Type soType)`,按关键事实映射填类型(未迁移的用基类 `typeof(EnemyAbilitySO)`):
|
||||||
|
```csharp
|
||||||
|
private static (string ablName, string ablId, System.Type soType)[] GetEnemyAbilityDefs(string enemyId) => enemyId switch
|
||||||
|
{
|
||||||
|
"E001" => new[] { ("Alert", "e001_alert", typeof(EnemyAbilitySO)),
|
||||||
|
("Chase", "e001_chase", typeof(ContactChaseAbilitySO)) },
|
||||||
|
"E002" => new[] { ("CeilingStrike", "e002_ceiling_strike", typeof(CeilingHangStrikeAbilitySO)) },
|
||||||
|
"E003" => new[] { ("Fall", "e003_fall", typeof(AnimatedCeilingDropAbilitySO)) },
|
||||||
|
"E004" => new[] { ("Appear", "e004_appear", typeof(AppearAbilitySO)),
|
||||||
|
("Bite", "e004_bite", typeof(EnemyAbilitySO)),
|
||||||
|
("HeadSlam", "e004_headslam", typeof(RepeatSlamAbilitySO)),
|
||||||
|
("Acid", "e004_acid", typeof(EnemyAbilitySO)),
|
||||||
|
("Flip", "e004_flip", typeof(FacePlayerAbilitySO)) },
|
||||||
|
"E005" => new[] { ("Bite", "e005_bite", typeof(EnemyAbilitySO)),
|
||||||
|
("Acid", "e005_acid", typeof(EnemyAbilitySO)) },
|
||||||
|
"E006" => new[] { ("Leap", "e006_leap", typeof(EnemyAbilitySO)),
|
||||||
|
("Chase", "e006_chase", typeof(ContactChaseAbilitySO)) },
|
||||||
|
_ => System.Array.Empty<(string, string, System.Type)>(),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
文件顶部若未 `using BaseGames.Enemies.Abilities;` 则补上(这些子类型在该命名空间)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: `CreateEnemyAbilitySO` 接受类型 + 调用点透传**
|
||||||
|
|
||||||
|
改签名与实现:
|
||||||
|
```csharp
|
||||||
|
private static void CreateEnemyAbilitySO(string enemyId, string ablName, string ablId, System.Type soType)
|
||||||
|
{
|
||||||
|
string dir = $"Assets/_Game/Data/Enemies/{enemyId}/Abilities";
|
||||||
|
string name = $"ABL_{enemyId}_{ablName}";
|
||||||
|
var so = EditorScaffoldUtils.CreateSOAsset(soType, dir, name) as EnemyAbilitySO;
|
||||||
|
if (so != null)
|
||||||
|
{
|
||||||
|
so.abilityId = ablId;
|
||||||
|
EditorUtility.SetDirty(so);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
更新两处调用点:
|
||||||
|
- 工厂按钮循环(约 280-285 行):`foreach (var (ablName, ablId, soType) in GetEnemyAbilityDefs(id))` 且捕获 `soType`,按钮 lambda 改 `CreateEnemyAbilitySO(id, capturedName, capturedId, capturedType)`(新增 `var capturedType = soType;`)。
|
||||||
|
- `CreateAllEnemySOs`(约 625-626 行):`foreach (var (ablName, ablId, soType) in GetEnemyAbilityDefs(id)) CreateEnemyAbilitySO(id, ablName, ablId, soType);`
|
||||||
|
- `RefreshSOStatus` 里若有 `foreach (var (ablName, ablId) in ...)`(约 773 行 items.Add)也改为解构三元组(忽略 `soType` 用弃元 `_`)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 编译门** → count=0。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交**
|
||||||
|
```bash
|
||||||
|
git add Assets/_Game/Scripts/Editor/Shared/EditorScaffoldUtils.cs Assets/_Game/Scripts/Editor/Character/CharacterWizardWindow.cs
|
||||||
|
git commit -m "feat(wizard): 敌人能力 SO 按能力类型创建正确子类(向导批量创建产出子类 SO)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 9: 端到端结构验证 + 自检 + 收尾
|
||||||
|
|
||||||
|
**Files:** 无(仅验证)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 结构验证(MCP)——子类 SO 有预期字段、能力已改读 SO**
|
||||||
|
|
||||||
|
`unity_execute_code`(port 7890):反射确认 8 个子类型存在且暴露预期字段;确认对应能力 MonoBehaviour **不再**有旧的 `_startClip/_dashSpeed/…` 序列化字段:
|
||||||
|
```csharp
|
||||||
|
var sb=new System.Text.StringBuilder();
|
||||||
|
var asm = System.AppDomain.CurrentDomain.GetAssemblies();
|
||||||
|
System.Type Find(string n)=>asm.SelectMany(a=>{try{return a.GetTypes();}catch{return new System.Type[0];}}).FirstOrDefault(x=>x.Name==n);
|
||||||
|
void CheckSO(string so, string[] fields){ var t=Find(so); if(t==null){sb.AppendLine(so+": MISSING"); return;} foreach(var f in fields){ var fi=t.GetField(f); sb.AppendLine(so+"."+f+": "+(fi!=null?fi.FieldType.Name:"MISSING")); } sb.AppendLine(so+" base="+t.BaseType.Name); }
|
||||||
|
CheckSO("ContactChaseAbilitySO", new[]{"startClip","loopClip","endClip","dashSpeed"});
|
||||||
|
CheckSO("RepeatSlamAbilitySO", new[]{"startClip","loopClip","endClip","hitActiveTime","slamCount","staggerDuration"});
|
||||||
|
CheckSO("CeilingHangStrikeAbilitySO", new[]{"strikeClip","loopClip","endClip","hangDuration"});
|
||||||
|
CheckSO("AnimatedCeilingDropAbilitySO", new[]{"fallLoopClip","fallGravityScale","maxFallTime","groundMask","recoveryTime"});
|
||||||
|
CheckSO("MeleeVulnerabilityAbilitySO", new[]{"attackClip","hitEnterT","hitExitT","staggerDuration"});
|
||||||
|
CheckSO("AppearAbilitySO", new[]{"appearClip"});
|
||||||
|
CheckSO("FacePlayerAbilitySO", new[]{"faceClip"});
|
||||||
|
CheckSO("PlayClipAbilitySO", new[]{"clip"});
|
||||||
|
// 能力 MonoBehaviour 不应再有旧序列化字段
|
||||||
|
var bindings = System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic;
|
||||||
|
void NoOldField(string comp, string oldField){ var t=Find(comp); var fi=t?.GetField(oldField, bindings); sb.AppendLine(comp+" 残留 "+oldField+"="+(fi!=null)); }
|
||||||
|
NoOldField("ContactChaseAbility","_startClip"); NoOldField("ContactChaseAbility","_dashSpeed");
|
||||||
|
NoOldField("RepeatSlamAbility","_slamCount"); NoOldField("AnimatedCeilingDropAbility","_groundMask");
|
||||||
|
NoOldField("MeleeVulnerabilityAbility","_hitEnterT"); NoOldField("AppearAbility","_appearClip");
|
||||||
|
return sb.ToString();
|
||||||
|
```
|
||||||
|
期望:各 SO 字段类型齐全、`base=EnemyAbilitySO`;各能力「残留 = False」。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 每个子类 `[CreateAssetMenu]` 可见性抽检**:在编辑器 `Assets > Create > BaseGames/Enemies/Abilities/` 下应能看到 8 个新菜单项(人工抽查 1-2 个,或反射确认类型带 `CreateAssetMenuAttribute`)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: SO 自检**:菜单 `BaseGames/Tools/Validation/Validate All ScriptableObjects`(`unity_execute_menu_item`)——期望无因新子类型产生的新错误(现有 `ABL_*.asset` 仍是基类,属用户待重建,若校验器对缺失字段报警属预期,记录即可,不在本 Task 修资产)。
|
||||||
|
|
||||||
|
- [ ] **Step 4: grep 确认能力脚本无残留迁移字段声明**
|
||||||
|
```
|
||||||
|
rg -n "SerializeField.*(ClipTransition|_dashSpeed|_slamCount|_hitActiveTime|_hangDuration|_fallGravityScale|_maxFallTime|_groundMask|_recoveryTime|_hitEnterT|_hitExitT|_staggerDuration)" Assets/_Game/Scripts/Enemies/Abilities/*.cs
|
||||||
|
```
|
||||||
|
期望:0 命中(8 个能力的迁移字段声明都已删除;`Configs/` 子目录里的是 public 字段,不带 `[SerializeField]`,不会命中)。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 更新记忆** `editor_scaffold_tools.md` 或新增记忆:记录「敌人能力参数现由 `XxxAbilitySO : EnemyAbilitySO` 子类承载;`EnemyAbilityBase.ResolveConfig<T>` 类型不符显式报错;场景组件引用(BodyContactDamage/HitBox/HurtBox)仍在 MonoBehaviour;向导按能力类型创建子类 SO;现有 ABL_ 资产需重建为子类」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review(作者已核对)
|
||||||
|
|
||||||
|
- **Spec 覆盖**:§A 子类 SO→Task 2-7(8 个子类);§B `ResolveConfig` 显式报错→Task 1;§C MonoBehaviour 删字段+读 SO+空守卫→Task 2-7;§D 脚手架/创建入口→Task 8;§E 迁移(用户重建)→全程不动资产 + Task 9 记录;验证→各 Task 编译门 + Task 1 单测 + Task 9 结构验证。场景引用保留→每个迁移 Task 显式「保留」。
|
||||||
|
- **占位符**:无 TBD;每个 SO 给完整文件;每个 MonoBehaviour 给「删哪些行 + 新字段/Awake 块 + 使用处替换清单 + 空守卫」;Task 8 给完整映射表与调用点。
|
||||||
|
- **类型一致**:`ResolveConfig<T>() where T : EnemyAbilitySO → T`;子类名 `ContactChaseAbilitySO/RepeatSlamAbilitySO/CeilingHangStrikeAbilitySO/AnimatedCeilingDropAbilitySO/MeleeVulnerabilityAbilitySO/AppearAbilitySO/FacePlayerAbilitySO/PlayClipAbilitySO` 贯穿 Task 2-9;字段名(`startClip/loopClip/endClip/dashSpeed/hitActiveTime/slamCount/staggerDuration/hangDuration/strikeClip/fallLoopClip/fallGravityScale/maxFallTime/groundMask/recoveryTime/attackClip/hitEnterT/hitExitT/appearClip/faceClip/clip`)在 SO 定义与 MonoBehaviour 使用处一致;`CreateSOAsset(Type,…)` 重载签名一致。
|
||||||
|
- **已知风险**:(1) `_config.attackSequence[...].damageSource` 保持用基类 `_config`(未改 `_cfg`),因 `_cfg` 可能因类型错为 null 而 `_config` 仍有效——已在相关 Task 显式「保持不变」。(2) EditMode 反射跑 `LogAssert` 可能需真 Test Runner——Task 1 Step 5 给了回退(编辑器 Test Runner 手动跑)。(3) 现有 17 个 `ABL_` 资产仍是基类,运行时会触发 `ResolveConfig` 报错——这是**预期的显式报错**,提示用户重建(符合 §6,非缺陷)。
|
||||||
Reference in New Issue
Block a user