Files
zeling_v2/Assets/_Game/Scripts/Enemies/Abilities/RushAbility.cs
T
joywayer 4808077fc6 feat(enemy): ContactChaseAbility 改造为 RushAbility(冲锋)
- 起手前锁定玩家 X 与方向(committed),终点改为身体整体越过锁定点(Body.HasPassed)
- 终止条件:越过锁定点/撞墙(只判墙)/最大冲刺时长(SO 新增 maxDashDuration)/净位移卡死
- 冲锋期间经 EnemyMoveInput.AllowLedgeCross 放开悬崖夹紧,可冲下崖不悬停边缘
- 类与文件重命名(GUID 保留),脚手架与向导同步更新
2026-07-27 13:38:05 +08:00

157 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
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.
using System.Collections;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 冲锋能力:起手(Skill_Start) → 朝**锁定点**直冲(Skill_Loop) → 收招(Skill_End)。
/// 一次冲锋为一次能力执行,结束后进入冷却(EnemyAbilitySO.cooldown)。
///
/// 冲锋目标在**起手动画之前**锁定:记下玩家当时的 X 与由此确定的方向,此后玩家怎么动都不改变本次冲锋
/// committed 冲锋——起手即预警,玩家可在起手期间跑开躲掉)。
///
/// 终点判据是"**身体整体越过**锁定点"<see cref="IEnemyBody.HasPassed"/>),即碰撞体后缘越过锁定 X,
/// 而非中心到达——身体宽度取自身体几何权威 <see cref="EnemyBase.Body"/>,本能力不自行解析碰撞体。
///
/// 终止条件(任一满足即收招并进入冷却):身体整体越过锁定点(正常完成)/前方有墙(只判墙,不判悬崖)/
/// 超过 <see cref="RushAbilitySO.maxDashDuration"/>(够不到锁定点时收尾:掉坑、对岸、被非检测层挡住)/
/// 净位移卡死(撞上未纳入墙层的碰撞体时兜底)。
///
/// 悬崖:冲锋期间**允许越过崖沿**(经 <see cref="EnemyMoveInput.AllowLedgeCross"/> 放开移动层的悬崖夹紧),
/// 敌人会冲出平台边缘并按重力下落、空中保持水平冲速,而不是停在崖边悬空。
///
/// 冲锋动画由本能力全权驱动(步态不抢动画)。身体接触伤害与本能力解耦:
/// 由常驻的 <see cref="BodyContactDamage"/> 全程负责,本能力不开关它。
/// </summary>
public class RushAbility : EnemyAbilityBase
{
private RushAbilitySO _cfg;
// 卡死检测:窗口时长与窗口内最小净位移(撞上未纳入墙层的碰撞体时兜底结束冲锋)
private const float StuckWindowSeconds = 0.15f;
private const float StuckMinProgress = 0.05f;
protected override void Awake()
{
base.Awake();
_cfg = ResolveConfig<RushAbilitySO>();
}
protected override IEnumerator ExecuteCoroutine()
{
if (_cfg == null) yield break;
Phase = AbilityRunState.Active;
_enemy.SetEngaged(true);
// ── 锁定:起手之前记下玩家 X 与方向,此后不再跟随(committed 冲锋,可被躲)──
var player = _enemy.PlayerTransform;
float dir = (player != null && player.position.x < _enemy.transform.position.x) ? -1f : 1f;
float lockX = player != null
? player.position.x
: _enemy.transform.position.x + dir; // 无玩家:朝当前朝向冲一个身位,仍走完整流程
var mv = _enemy.Movement;
// 先转身朝锁定方向(背对时播转身动画),转身完成后再起手
if (mv != null)
{
mv.FaceDirection((int)dir);
while (mv.IsTurning)
yield return null;
}
// 起手:一次性预警动画。期间静止;锁住动画防被步态覆盖。
if (_cfg.startClip.Clip != null)
{
_enemy.LockAnim(_cfg.startClip.Clip.length);
_animancer.Play(_cfg.startClip);
yield return EnemyAbilityWaits.Get(_cfg.startClip.Clip.length);
}
// 冲刺速度:优先能力配置的 dashSpeed,否则回退 Stats.RunSpeed
float speed = _cfg.dashSpeed > 0f ? _cfg.dashSpeed
: (_enemy.Stats != null ? _enemy.Stats.RunSpeed : 0f);
if (_cfg.loopClip.Clip != null)
_animancer.Play(_cfg.loopClip);
yield return RushRoutine(dir, lockX, speed, mv);
_enemy.StopMovement();
// 收招 Skill_End;随后基类按 EnemyAbilitySO.cooldown 施加冷却。
yield return PlaySkillEnd();
Cleanup();
}
/// <summary>
/// 冲锋推进:每帧朝锁定方向施加冲速(允许越崖),
/// 直到身体整体越过锁定点/撞墙/超时/卡死。
/// </summary>
private IEnumerator RushRoutine(float dir, float lockX, float speed, EnemyMovement mv)
{
var body = _enemy.Body;
float elapsed = 0f;
float winX = _enemy.transform.position.x;
float winT = 0f;
while (true)
{
// 终点:身体整体越过锁定点(后缘越过;宽度由身体权威提供)
if (body != null && body.HasPassed(lockX, dir)) break;
// 撞墙即止(悬崖不再终止——冲锋要能冲下去)
if (mv != null && mv.WouldHitWallAhead(dir)) break;
// 超时收尾:够不到锁定点(掉坑/对岸/被非检测层挡住)
if (_cfg.maxDashDuration > 0f && elapsed >= _cfg.maxDashDuration) break;
_enemy.MoveInDirectionWithSpeed(dir, speed, allowLedgeCross: true);
yield return null;
float dt = Time.deltaTime;
elapsed += dt;
winT += dt;
// 兜底:撞上未纳入墙层的碰撞体(玩家/边界墙)导致原地不动 → 结束本次冲锋。
// 用窗口净位移判定,避免"贴住微抖"每帧重置。
if (winT >= StuckWindowSeconds)
{
if (Mathf.Abs(_enemy.transform.position.x - winX) < StuckMinProgress) break;
winX = _enemy.transform.position.x;
winT = 0f;
}
}
}
protected override void OnInterrupted(InterruptReason reason)
{
// 丢失目标 = AI 主动退出(ExternalRequest) → 播收招 Skill_End
// 受击/死亡/硬直等硬打断不播收招。协程已停,fire-and-forget;动画锁保证 Skill_End
// 在其时长内不被巡逻步态盖掉。
if (reason == InterruptReason.ExternalRequest && _cfg != null && _cfg.endClip.Clip != null)
{
_enemy.LockAnim(_cfg.endClip.Clip.length);
_animancer.Play(_cfg.endClip);
}
Cleanup();
}
private IEnumerator PlaySkillEnd()
{
if (_cfg == null || _cfg.endClip.Clip == null) yield break;
_enemy.LockAnim(_cfg.endClip.Clip.length);
_animancer.Play(_cfg.endClip);
yield return EnemyAbilityWaits.Get(_cfg.endClip.Clip.length);
}
private void Cleanup()
{
_enemy.SetEngaged(false);
_enemy.Locomotion?.Stop(); // 经 StopMovement 路径一并复位 AllowLedgeCross
}
}
}