using System; using System.Collections; using UnityEngine; using BaseGames.Input; using BaseGames.Core.Events; namespace BaseGames.Parry { /// /// 弹反阶段枚举(架构 06_CombatModule §9)。 /// public enum ParryPhase { Inactive, // 待机 Startup, // 前摇(按键 → 有效窗口开启) Active, // 弹反有效窗口 EndLag, // 后摇(窗口关闭 → 动作恢复) CounterWindow, // 弹反成功后的反击窗口 } /// /// 弹反状态机(架构 06_CombatModule §9)。 /// /// 组件设计: /// - 订阅 InputReaderSO.ParryEvent → 触发 TryActivateParry /// - 触发 OnParryActivated C# 事件 → PlayerController 转换到 ParryState /// - HurtBox 调用 ConsumeParry() → 弹反成功时触发奖励并进入 CounterWindow /// - 触发 OnParryConsumed(ParryInfo) C# 事件 → PlayerController 发放灵力 / 恢复护盾 /// - 可选触发 ParryInfoEventChannelSO SO 事件 → UI / 反馈系统 /// /// 程序集约束:BaseGames.Parry 不引用 BaseGames.Combat → ConsumeParry() 无 DamageInfo 参数。 /// public class ParrySystem : MonoBehaviour { [Header("配置")] [SerializeField] private ParryConfigSO _config; // _inputReader 由 PlayerController.Awake 注入,无需在 Inspector 单独配置。 private InputReaderSO _inputReader; [Header("Event Channels - Raise(可选)")] [SerializeField] private ParryInfoEventChannelSO _onParrySuccess; // ── 运行时状态 ──────────────────────────────────────────────────────── private ParryPhase _phase = ParryPhase.Inactive; private float _phaseTimer; private float _cooldownTimer; // 缓存 WaitForSecondsRealtime,避免每次完美弹反都触发 GC 分配 private WaitForSecondsRealtime _bulletTimeWait; public ParryPhase CurrentPhase => _phase; /// 是否处于弹反有效窗口(供外部检测)。 public bool IsParrying => _phase == ParryPhase.Active; public bool IsInCounterWindow => _phase == ParryPhase.CounterWindow; /// 启用/禁用弹反输入(玩家能力解锁前设为 false)。 public bool IsEnabled { get; set; } = true; // ── C# 事件 ─────────────────────────────────────────────────────────── /// /// 弹反成功时触发(ConsumeParry 返回 true 后)。 /// PlayerController 订阅此事件以发放灵力并恢复护盾。 /// public event Action OnParryConsumed; /// /// 弹反输入激活时触发(进入 Startup 阶段时)。 /// PlayerController 订阅此事件以转换到 ParryState。 /// public event Action OnParryActivated; // ── Unity 生命周期 ──────────────────────────────────────────────────── private void Awake() { Debug.Assert(_config != null, "[ParrySystem] _config 未赋值,请在 Inspector 中指定 ParryConfigSO。", this); } private void Start() { // 在 Start 中初始化,确保 _config 已被赋值(Awake 之后) if (_config != null) _bulletTimeWait = new WaitForSecondsRealtime(_config.BulletTimeDuration); } /// 由 PlayerController 在 Awake 中注入 InputReader,无需在 Inspector 单独指定。 public void SetInputReader(InputReaderSO reader) { _inputReader = reader; } private void OnEnable() { if (_inputReader != null) _inputReader.ParryEvent += TryActivateParry; } private void OnDisable() { if (_inputReader != null) _inputReader.ParryEvent -= TryActivateParry; } private void Update() { // 冷却计时(使用 unscaledDeltaTime,子弹时间期间也正常递减) if (_cooldownTimer > 0f) _cooldownTimer -= Time.unscaledDeltaTime; if (_phase == ParryPhase.Inactive) return; _phaseTimer -= Time.unscaledDeltaTime; if (_phaseTimer <= 0f) AdvancePhase(); } // ── 外部 API ────────────────────────────────────────────────────────── /// /// HurtBox.ReceiveDamage 在步骤 2 调用此方法。 /// 若处于 Active 阶段则触发弹反成功流程并返回 true;否则返回 false。 /// public bool ConsumeParry() { if (_phase != ParryPhase.Active) return false; bool isPerfect = IsInPerfectWindow(); int soulGain = isPerfect ? _config.SoulGainOnParry + _config.SoulGainOnPerfect : _config.SoulGainOnParry; // 完美弹反子弹时间 if (isPerfect) StartCoroutine(ApplyBulletTime()); // 构造负载 var info = new ParryInfo { IsPerfect = isPerfect, SoulGained = soulGain }; // SO 事件(UI / 特效 / 音效系统订阅) _onParrySuccess?.Raise(info); // C# 事件(PlayerController 订阅以添加灵力 + 恢复护盾) OnParryConsumed?.Invoke(info); // 进入反击窗口 EnterPhase(ParryPhase.CounterWindow, _config.CounterWindowDuration); return true; } /// /// 由状态机(ParryState.OnStateEnter)或动画事件直接触发,跳过 Startup 前摇直接进入 Active 窗口。 /// 若弹反已处于任意活跃阶段则忽略。 /// public void OpenParryWindow() { if (_phase != ParryPhase.Inactive) return; EnterPhase(ParryPhase.Active, _config.WindowDuration); } /// /// 强制中断弹反(ParryState.OnStateExit 或被击中打断时调用)。 /// public void CloseParryWindow() { if (_phase == ParryPhase.Inactive) return; _cooldownTimer = _config.ParryCooldown; EnterPhase(ParryPhase.Inactive, 0f); } // ── 私有实现 ────────────────────────────────────────────────────────── private void TryActivateParry() { if (!IsEnabled) return; if (_phase != ParryPhase.Inactive) return; if (_cooldownTimer > 0f) return; EnterPhase(ParryPhase.Startup, _config.StartupDuration); OnParryActivated?.Invoke(); } private bool IsInPerfectWindow() { // Active 阶段计时器从 WindowDuration 倒计,elapsed = WindowDuration - _phaseTimer float elapsed = _config.WindowDuration - _phaseTimer; return elapsed <= _config.PerfectParryThreshold; } private void AdvancePhase() { switch (_phase) { case ParryPhase.Startup: EnterPhase(ParryPhase.Active, _config.WindowDuration); break; case ParryPhase.Active: EnterPhase(ParryPhase.EndLag, _config.EndlagDuration); break; case ParryPhase.EndLag: case ParryPhase.CounterWindow: _cooldownTimer = _config.ParryCooldown; EnterPhase(ParryPhase.Inactive, 0f); break; } } private void EnterPhase(ParryPhase phase, float duration) { _phase = phase; _phaseTimer = duration; } private IEnumerator ApplyBulletTime() { Time.timeScale = _config.BulletTimeScale; yield return _bulletTimeWait; Time.timeScale = 1f; } } }