feat(enemy): E001 追击动画 Skill_Start/Loop/End 完整化,符合设计文档

依据 Docs/Game/Design/小怪设计-程序开发文档-01.md:感知→Skill_Start(张口,单次)
→Skill_Loop(快速爬行追击,循环)→丢失目标→Skill_End(收招,单次)→Move_Patrol,
E001 无警觉态。

原问题:ContactChaseAbility 的 loop/end clip 都空、无 start clip,追击全程无
动画;Approach 步态还错播 Move;有多余警觉态(Alert clip 也空)。

- ContactChaseAbility:加 _startClip;协程 Skill_Start(锁动画+等时长,不伤人)→
  开伤害+Approach+Skill_Loop(循环)→退出播 Skill_End。丢失目标(ExternalRequest
  中断)播 Skill_End;受击/死亡硬打断不播。
- EnemyBase:加动画锁 LockAnim/IsAnimLocked——一次性招式动画期间步态不覆盖;
  PlayLocomotionClip 的 Approach 改为不驱动步态(追击动画归能力)。
- EnemyLocomotion:锁定期不算"已应用",解锁后重同步步态(避免卡在 Skill_End)。
- ENM_E001_Stats:HasAlertState=false(设计无警觉态)。
- prefab + 脚手架:ContactChaseAbility 接线 Skill_Start/Loop/End;新增
  AssignClipTransition 助手(按名+路径定位同名 clip)。

PlayMode 验证:Skill_Start(locked/不伤人)→Skill_Loop(伤害开)→Skill_End
(locked)→步态恢复 Move,全链符合设计。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
2026-07-10 16:27:02 +08:00
parent f1ca62f881
commit 36aa2006e4
6 changed files with 190 additions and 109 deletions
@@ -519,6 +519,10 @@ namespace BaseGames.Editor
AssignAsset(chaseAbility, "_config", report, false, "ABL_E001_Chase");
AssignReference(chaseAbility, "_contactDamage", bodyContact, report);
// 追击动画:起手/循环/收招(设计文档 Skill_Start/Loop/End
AssignClipTransition(chaseAbility, "_startClip", "Skill_Start", "Enemies/E001", report);
AssignClipTransition(chaseAbility, "_loopClip", "Skill_Loop", "Enemies/E001", report);
AssignClipTransition(chaseAbility, "_endClip", "Skill_End", "Enemies/E001", report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
@@ -2292,6 +2296,29 @@ namespace BaseGames.Editor
so.ApplyModifiedPropertiesWithoutUndo();
}
/// <summary>
/// 给组件上的 Animancer <c>ClipTransition</c> 字段赋 AnimationClip(写嵌套 <c>_Clip</c>)。
/// 按 clip 名 + 路径子串定位(同名 clip 各敌人各有一份,须用路径区分,如 "Enemies/E001")。
/// </summary>
private static void AssignClipTransition(Object target, string propName, string clipName, string pathContains, List<string> report)
{
AnimationClip clip = null;
foreach (var g in AssetDatabase.FindAssets(clipName + " t:AnimationClip"))
{
var p = AssetDatabase.GUIDToAssetPath(g);
if (System.IO.Path.GetFileNameWithoutExtension(p) == clipName && p.Contains(pathContains))
{ clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(p); break; }
}
if (clip == null) { report?.Add($"★ 缺少动画 clip{clipName}(路径含 {pathContains}"); return; }
var so = new SerializedObject(target);
var sp = so.FindProperty(propName);
var cp = sp?.FindPropertyRelative("_Clip");
if (cp == null) { report?.Add($"{target.GetType().Name}.{propName}._Clip 字段不存在,跳过 clip 赋值。"); return; }
cp.objectReferenceValue = clip;
so.ApplyModifiedPropertiesWithoutUndo();
}
private static void AssignAsset(Object target, string propName, List<string> report, bool required, params string[] candidates)
{
Object asset = FindFirstAsset(candidates);