using UnityEngine; using BaseGames.Combat; namespace BaseGames.Player.States { /// /// 地面攻击状态(任意段连击)。 /// 攻速倍率(Stats.AnimatorSpeedMultiplier)缩放 clip 播放速度及 recoveryTime/comboTimeout。 /// 状态机流程: /// 播放动画 → [comboInputOpen] 接受输入 → [cancelWindowOpen] 允许跳跃/冲刺 → /// 动画结束 → 硬直(recoveryTime) → 连击等待(comboTimeout) → 无输入则回 Idle /// 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()); 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()); 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()); 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) } // 进入动画后等待阶段 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()); return; } var ds = Owner.GetState(); if (ds != null && ds.CanDash && Stats != null && Stats.HasAbility(AbilityType.Dash) && Buffer.ConsumeDash()) { Owner.TransitionTo(ds); } } private void OnAttackInput() { if (_comboWindowOpen || _waitingAfterAnim) _comboInputPending = true; } } }