227 lines
8.6 KiB
C#
227 lines
8.6 KiB
C#
using UnityEngine;
|
||
using BaseGames.Combat;
|
||
|
||
namespace BaseGames.Player.States
|
||
{
|
||
/// <summary>
|
||
/// 地面攻击状态(任意段连击)。
|
||
/// 攻速倍率(Stats.AnimatorSpeedMultiplier)缩放 clip 播放速度及 recoveryTime/comboTimeout。
|
||
/// 状态机流程:
|
||
/// 播放动画 → [comboInputOpen] 接受输入 → [cancelWindowOpen] 允许跳跃/冲刺 →
|
||
/// 动画结束 → 硬直(recoveryTime) → 连击等待(comboTimeout) → 无输入则回 Idle
|
||
/// </summary>
|
||
public class AttackState : PlayerStateBase
|
||
{
|
||
private int _comboIndex;
|
||
private bool _comboInputPending; // 连击窗口内已收到攻击输入
|
||
private bool _comboWindowOpen; // 当前是否接受连击输入
|
||
|
||
// 动画结束后的两阶段计时
|
||
private bool _waitingAfterAnim; // 是否在动画结束后等待阶段
|
||
private float _recoveryEndTime; // 硬直结束时刻
|
||
private float _comboTimeoutEnd; // 连击等待结束时刻
|
||
|
||
public AttackState(PlayerController owner) : base(owner) { }
|
||
|
||
public override void OnStateEnter()
|
||
{
|
||
_comboIndex = 0;
|
||
_comboInputPending = false;
|
||
_comboWindowOpen = false;
|
||
_waitingAfterAnim = false;
|
||
Input.AttackEvent += OnAttackInput;
|
||
PlayAttackClip();
|
||
}
|
||
|
||
public override void OnStateExit()
|
||
{
|
||
Input.AttackEvent -= OnAttackInput;
|
||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||
Move?.SetCancelWindowOpen(false);
|
||
}
|
||
|
||
public override void OnStateUpdate()
|
||
{
|
||
if (!_waitingAfterAnim)
|
||
{
|
||
// ── 动画播放中:只处理取消窗口(跳跃/冲刺打断)──────────────
|
||
if (!Move.CancelWindowOpen) return;
|
||
TryConsumeCancelInput();
|
||
return;
|
||
}
|
||
|
||
// ── 动画结束后等待阶段 ─────────────────────────────────────────
|
||
float now = Time.time;
|
||
|
||
// 硬直结束后开放取消窗口
|
||
if (!Move.CancelWindowOpen && now >= _recoveryEndTime)
|
||
Move.SetCancelWindowOpen(true);
|
||
|
||
// 有缓存的连击输入 → 立即推进
|
||
if (_comboInputPending)
|
||
{
|
||
AdvanceCombo();
|
||
return;
|
||
}
|
||
|
||
// 连击超时 → 返回 Idle
|
||
if (now >= _comboTimeoutEnd)
|
||
{
|
||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||
return;
|
||
}
|
||
|
||
// 恢复期结束后仍可跳跃/冲刺取消
|
||
if (Move.CancelWindowOpen)
|
||
TryConsumeCancelInput();
|
||
}
|
||
|
||
public override void OnStateFixedUpdate()
|
||
{
|
||
// 攻击动画播放期间无水平输入,仍需每帧调用 Move(0) 叠加 _platformVelocity.x,
|
||
// 使玩家在移动平台上攻击时仍随平台同步移动。
|
||
if (Move.IsGrounded)
|
||
Move.Move(0f);
|
||
}
|
||
|
||
// ── 内部 ──────────────────────────────────────────────────────────────
|
||
|
||
private void PlayAttackClip()
|
||
{
|
||
_waitingAfterAnim = false;
|
||
_comboWindowOpen = false;
|
||
Move.SetCancelWindowOpen(false);
|
||
|
||
var weapon = Owner.Weapon?.ActiveWeapon;
|
||
if (weapon == null)
|
||
{
|
||
UnityEngine.Debug.LogWarning("[AttackState] 未找到 ActiveWeapon,请检查 WeaponManager 配置。");
|
||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||
return;
|
||
}
|
||
|
||
var step = weapon.GetGroundStep(_comboIndex);
|
||
if (step.clip == null || step.clip.Clip == null)
|
||
{
|
||
UnityEngine.Debug.LogWarning($"[AttackState] 连击段 {_comboIndex} 动画未配置,请检查 {weapon.weaponId}。");
|
||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||
return;
|
||
}
|
||
|
||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||
var animState = Anim.Play(step.clip);
|
||
animState.Speed *= spd; // 在 ClipTransition 自身速度基础上叠加攻速
|
||
|
||
// 每次重播同一 ClipTransition 会复用同一 AnimancerState,
|
||
// 必须先清除旧事件再注册新事件,否则回调会累积叠加。
|
||
var events = animState.Events(this);
|
||
events.Clear();
|
||
events.OnEnd = OnClipEnd;
|
||
|
||
// HitBox 时间窗口(capture step by value for closure safety)
|
||
var capturedStep = step;
|
||
events.Add(capturedStep.hitBoxEnter, () =>
|
||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground,
|
||
capturedStep.hitBoxId, capturedStep.damageSource));
|
||
events.Add(capturedStep.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||
|
||
// 连击输入窗口
|
||
if (capturedStep.comboInputOpen > 0f)
|
||
{
|
||
events.Add(capturedStep.comboInputOpen, () =>
|
||
{
|
||
_comboWindowOpen = true;
|
||
// 窗口刚开时,补检查 InputBuffer——玩家可能在窗口前就提前按键
|
||
if (!_comboInputPending && Buffer.ConsumeAttack())
|
||
_comboInputPending = true;
|
||
});
|
||
}
|
||
else
|
||
{
|
||
_comboWindowOpen = true;
|
||
if (!_comboInputPending && Buffer.ConsumeAttack())
|
||
_comboInputPending = true;
|
||
}
|
||
|
||
if (capturedStep.comboInputClose > 0f)
|
||
events.Add(capturedStep.comboInputClose, () => _comboWindowOpen = false);
|
||
|
||
// 取消窗口(跳跃/冲刺)
|
||
if (capturedStep.cancelWindowOpen > 0f)
|
||
events.Add(capturedStep.cancelWindowOpen, () => Move.SetCancelWindowOpen(true));
|
||
}
|
||
|
||
private void OnClipEnd()
|
||
{
|
||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||
_comboWindowOpen = false;
|
||
Move.SetCancelWindowOpen(false);
|
||
|
||
// 有缓存连击输入且还不是最后一段 → 零延迟推进到下一段
|
||
if (_comboInputPending)
|
||
{
|
||
_comboInputPending = false;
|
||
int maxCombo = Owner.Weapon?.ActiveWeapon?.GroundComboCount ?? 1;
|
||
if (_comboIndex < maxCombo - 1)
|
||
{
|
||
_comboIndex++;
|
||
PlayAttackClip();
|
||
return; // 新动画已开始,不进入等待阶段
|
||
}
|
||
// 已是最后一段:消耗掉多余输入,继续进入等待阶段(不 return)
|
||
}
|
||
|
||
// 进入动画后等待阶段。
|
||
// 必须播放新动画(Idle),否则 Animancer End Event 会在每帧重复触发 OnClipEnd。
|
||
if (AnimCfg?.Idle != null)
|
||
Anim.Play(AnimCfg.Idle);
|
||
|
||
var step = Owner.Weapon?.ActiveWeapon?.GetGroundStep(_comboIndex) ?? default;
|
||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||
float now = Time.time;
|
||
|
||
_waitingAfterAnim = true;
|
||
_recoveryEndTime = now + step.recoveryTime / spd;
|
||
_comboTimeoutEnd = _recoveryEndTime + step.comboTimeout / spd;
|
||
}
|
||
|
||
private void AdvanceCombo()
|
||
{
|
||
int maxCombo = Owner.Weapon?.ActiveWeapon?.GroundComboCount ?? 1;
|
||
if (_comboIndex < maxCombo - 1)
|
||
{
|
||
_comboIndex++;
|
||
_comboInputPending = false;
|
||
PlayAttackClip();
|
||
}
|
||
else
|
||
{
|
||
// 已是最后一段,忽略多余输入,等待超时回 Idle
|
||
_comboInputPending = false;
|
||
}
|
||
}
|
||
|
||
private void TryConsumeCancelInput()
|
||
{
|
||
if (Buffer.ConsumeJump())
|
||
{
|
||
Owner.TransitionTo(Owner.GetState<JumpState>());
|
||
return;
|
||
}
|
||
var ds = Owner.GetState<DashState>();
|
||
if (ds != null && ds.CanDash
|
||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||
&& Buffer.ConsumeDash())
|
||
{
|
||
Owner.TransitionTo(ds);
|
||
}
|
||
}
|
||
|
||
private void OnAttackInput()
|
||
{
|
||
if (_comboWindowOpen || _waitingAfterAnim)
|
||
_comboInputPending = true;
|
||
}
|
||
}
|
||
}
|