Files
zeling_v2/Assets/_Game/Scripts/Player/States/AttackState.cs

200 lines
7.4 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 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 自身速度基础上叠加攻速
var events = animState.Events(this);
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);
else
_comboWindowOpen = true; // 0 = 立即开放
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)
{
AdvanceCombo();
return;
}
// 进入动画后等待阶段
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
{
// 已是最后一段,忽略多余输入,等待超时
_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;
}
}
}