feat(combat): 接通弹反反制——弹反成功让发起攻击的敌人硬直

EnemyBase.ReceiveParry 早就存在且形态正确(强制 Stagger + 打断全部能力 +
定时恢复,小怪 Boss 通用),但一直零调用者:弹反判定点拿不到攻击者引用。
硬直时长的权威也早就存在——ParryConfigSO.StaggerDuration,注释写着
「被弹反敌人的受击硬直时长」,同样零消费者。本次只是把这两端接上,没有新造语义。

接缝选在 DamageInfo 而非"弹反成功时回调 HitBox"(spec §6.2 留的两个选项):
攻击者引用与既有的 SourceProjectile 完全同构——都是"攻击从哪来"的引用,
供弹反分支反向作用于来源。落到代码上就是弹反分支里并列的两行,不引入第二条回调链路。

- Combat 新增 IParryable(攻击被弹反时的反制承受方)。用接口是因为程序集方向:
  Combat 不能反向依赖 Enemies,而 HurtBox.ReceiveDamage 是唯一同时握有
  弹反结果与 DamageInfo 的地方。与 IDamageable 同款做法。
- DamageInfo 加 Attacker 字段([NonSerialized],Builder 与 From 工厂同步支持)。
- HitBox 在 Activate 时按宿主解析并缓存 IParryable——与 _ownerRigidbody 同源、
  与 _ownerProjectile 同手法。放 Activate 而非 Awake:attacker 可由调用方传入。
- ParrySystem 暴露 StaggerDuration,HurtBox 在弹反成功时读它,不另立常量。
- EnemyBase 声明实现 IParryable(方法签名本就匹配,无需改实现)。

顺带确认旧的全局误伤路径确实已随 Boss 单轨合并删净:全库不再有
HandleParrySuccess,ParryInfoEventChannelSO 只剩 ParrySystem 自己 Raise,
不存在"弹反小怪导致场上 Boss 一起硬直"的双重路径。

验证:编译 0 错;EditMode 276/276(273 + 新增 3)。
测试走完整 HurtBox 流水线:反射仅用于喂配置与跑真实 Awake,被测逻辑全程生产代码
(与 BossPhaseAbilityGateTests 同一手法)。硬直时长断言取 1.25f 这个非默认值,
才能证明读的是配置而非常量。用例把完美弹反阈值设为负数以避开会改 Time.timeScale
的子弹时间协程(编辑模式下它 yield 后不会恢复),TearDown 另有兜底还原。
变异验证——注释掉 HurtBox 里那一行后,恰好只有反制用例变红;还原后复验 276/276。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 11:36:52 +08:00
co-authored by Claude Opus 5
parent 1e3c060c8d
commit bf7899f91a
9 changed files with 207 additions and 5 deletions
+9 -1
View File
@@ -93,6 +93,10 @@ namespace BaseGames.Combat
// 宿主投射物缓存(Activate 时填入,DamageInfo.SourceProjectile 写入用)
private Projectile _ownerProjectile;
// 宿主的弹反反制承受方(Activate 时填入,DamageInfo.Attacker 写入用)。
// 弹反成功时由 HurtBox 反向调用它的 ReceiveParry。
private IParryable _ownerParryable;
/// <summary>
/// 激活 HitBox。source/attacker 均可选,未传则使用 Inspector 默认值。
/// ⚠️ 不存在 Activate(float duration) 重载。
@@ -106,6 +110,9 @@ namespace BaseGames.Combat
_attackerTransform = attacker ?? transform;
_isActive = true;
_ownerRigidbody = _attackerTransform.GetComponentInParent<Rigidbody2D>();
// 攻击者的弹反反制承受方;与 _ownerRigidbody 同源解析,供 DamageInfo.Attacker 写入。
// 放 Activate 而非 Awakeattacker 可由调用方传入,宿主要到这时才确定。
_ownerParryable = _attackerTransform.GetComponentInParent<IParryable>();
foreach (var col in _directColliders) col.enabled = true;
foreach (var proxy in _proxies) proxy.SetEnabled(true);
// 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标)
@@ -265,7 +272,8 @@ namespace BaseGames.Combat
knockDir,
_attackerTransform.position,
_attackerTransform.gameObject.layer,
_ownerProjectile);
_ownerProjectile,
_ownerParryable);
info.HitActivationId = _currentActivationId;
// hitPoint:优先使用触发命中的碰撞体中心在目标表面的最近点;