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:
@@ -4,6 +4,7 @@
|
|||||||
"references": [
|
"references": [
|
||||||
"BaseGames.Combat.StatusEffects",
|
"BaseGames.Combat.StatusEffects",
|
||||||
"BaseGames.Combat",
|
"BaseGames.Combat",
|
||||||
|
"BaseGames.Parry",
|
||||||
"BaseGames.Core",
|
"BaseGames.Core",
|
||||||
"BaseGames.AI",
|
"BaseGames.AI",
|
||||||
"BaseGames.Enemies",
|
"BaseGames.Enemies",
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.TestTools;
|
||||||
|
using BaseGames.Combat;
|
||||||
|
using BaseGames.Parry;
|
||||||
|
using BaseGames.Enemies;
|
||||||
|
|
||||||
|
namespace BaseGames.Tests.EditMode.Combat
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 弹反反制接线:玩家弹反成功时,发起这次攻击的敌人必须硬直。
|
||||||
|
///
|
||||||
|
/// EnemyBase.ReceiveParry 早就存在且形态正确(强制 Stagger + 打断能力 + 定时恢复),
|
||||||
|
/// 但一直零调用者——弹反管线(HurtBox → ParrySystem.ConsumeParry)拿不到攻击者引用。
|
||||||
|
/// 硬直时长的权威也早就存在:ParryConfigSO.StaggerDuration,注释写着
|
||||||
|
/// 「被弹反敌人的受击硬直时长」,同样零消费者。本次是把这两端接上,不新造语义。
|
||||||
|
/// </summary>
|
||||||
|
public class ParryCounterWiringTests
|
||||||
|
{
|
||||||
|
/// <summary>攻击方探针:记录是否被通知、以及拿到的硬直时长。</summary>
|
||||||
|
private sealed class SpyAttacker : MonoBehaviour, IParryable
|
||||||
|
{
|
||||||
|
public int Calls;
|
||||||
|
public float LastDuration = -1f;
|
||||||
|
|
||||||
|
public void ReceiveParry(float staggerDuration)
|
||||||
|
{
|
||||||
|
Calls++;
|
||||||
|
LastDuration = staggerDuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>受击方:HurtBox.Awake 要在父级找到 IDamageable,否则流水线首步就返回。</summary>
|
||||||
|
private sealed class FakeVictim : MonoBehaviour, IDamageable
|
||||||
|
{
|
||||||
|
public bool IsAlive => true;
|
||||||
|
public bool IsInvincible => false;
|
||||||
|
public int Defense => 0;
|
||||||
|
public void TakeDamage(DamageInfo info) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameObject _victim;
|
||||||
|
private GameObject _attacker;
|
||||||
|
private ParryConfigSO _config;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp() => LogAssert.ignoreFailingMessages = true;
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
// ParrySystem 的完美弹反会改 Time.timeScale 后 yield,编辑模式下协程不会恢复。
|
||||||
|
// 本用例已用配置避开那条路径,这里再兜一道,避免任何意外把编辑器留在慢放状态。
|
||||||
|
Time.timeScale = 1f;
|
||||||
|
|
||||||
|
// 项目关闭了 Domain/Scene Reload:断言失败会跳过用例后续语句,
|
||||||
|
// 清理必须放在 TearDown,否则失败一次就往编辑器场景里漏一个对象。
|
||||||
|
if (_victim != null) Object.DestroyImmediate(_victim);
|
||||||
|
if (_attacker != null) Object.DestroyImmediate(_attacker);
|
||||||
|
if (_config != null) Object.DestroyImmediate(_config);
|
||||||
|
_victim = null; _attacker = null; _config = null;
|
||||||
|
LogAssert.ignoreFailingMessages = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 反射只用来「喂配置」与「跑真实的 Awake」,被测逻辑全程走生产代码。
|
||||||
|
// 与 BossPhaseAbilityGateTests 里给私有序列化字段喂数据是同一手法。
|
||||||
|
private static void SetPrivateField(object target, string field, object value)
|
||||||
|
=> target.GetType()
|
||||||
|
.GetField(field, BindingFlags.NonPublic | BindingFlags.Instance)
|
||||||
|
.SetValue(target, value);
|
||||||
|
|
||||||
|
private static void InvokeAwake(object target)
|
||||||
|
=> target.GetType()
|
||||||
|
.GetMethod("Awake", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||||
|
.Invoke(target, null);
|
||||||
|
|
||||||
|
/// <summary>建一个「弹反窗口已开启」的受击方。</summary>
|
||||||
|
private HurtBox MakeParryingVictim()
|
||||||
|
{
|
||||||
|
_config = ScriptableObject.CreateInstance<ParryConfigSO>();
|
||||||
|
// 完美弹反会 StartCoroutine 改 timeScale;窗口刚开时 elapsed==0 必然判定为完美。
|
||||||
|
// 阈值设为负数即可让 IsInPerfectWindow 恒假,把用例限制在普通弹反这条路径上。
|
||||||
|
_config.PerfectParryThreshold = -1f;
|
||||||
|
_config.StaggerDuration = 1.25f; // 与默认值不同,才能证明读的是配置而非常量
|
||||||
|
|
||||||
|
_victim = new GameObject("victim");
|
||||||
|
_victim.AddComponent<BoxCollider2D>(); // HurtBox 的 RequireComponent 指向抽象 Collider2D
|
||||||
|
_victim.AddComponent<FakeVictim>();
|
||||||
|
var hurtBox = _victim.AddComponent<HurtBox>();
|
||||||
|
Assert.IsNotNull(hurtBox, "HurtBox 需要先有具体 Collider2D 才能挂载");
|
||||||
|
InvokeAwake(hurtBox); // 解析 _owner 并建 HurtBoxOwnerGuard
|
||||||
|
|
||||||
|
var parry = _victim.AddComponent<ParrySystem>();
|
||||||
|
SetPrivateField(parry, "_config", _config);
|
||||||
|
hurtBox.SetParrySystem(parry);
|
||||||
|
parry.OpenParryWindow(); // 生产 API:动画事件走的就是这个
|
||||||
|
Assert.IsTrue(parry.IsParrying, "前提:弹反窗口必须已开启,否则测不到弹反分支");
|
||||||
|
|
||||||
|
return hurtBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DamageInfo ParryableHit(IParryable attacker)
|
||||||
|
=> new DamageInfo.Builder()
|
||||||
|
.SetRaw(10)
|
||||||
|
.SetFlags(DamageFlags.CanBeParried)
|
||||||
|
.SetAttacker(attacker)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ParrySuccess_StaggersTheAttacker_WithConfiguredDuration()
|
||||||
|
{
|
||||||
|
var hurtBox = MakeParryingVictim();
|
||||||
|
_attacker = new GameObject("attacker");
|
||||||
|
var spy = _attacker.AddComponent<SpyAttacker>();
|
||||||
|
|
||||||
|
hurtBox.ReceiveDamage(ParryableHit(spy));
|
||||||
|
|
||||||
|
Assert.AreEqual(1, spy.Calls,
|
||||||
|
"弹反成功必须反制发起这次攻击的那一方——这正是此前缺失的那根线");
|
||||||
|
Assert.AreEqual(_config.StaggerDuration, spy.LastDuration,
|
||||||
|
"硬直时长要取 ParryConfigSO.StaggerDuration,不得另立常量");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ParrySuccess_WithoutAttacker_DoesNotThrow()
|
||||||
|
{
|
||||||
|
// 陷阱 / 环境伤害等旁路没有攻击者,弹反它们不该炸。
|
||||||
|
var hurtBox = MakeParryingVictim();
|
||||||
|
|
||||||
|
Assert.DoesNotThrow(() => hurtBox.ReceiveDamage(ParryableHit(null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EnemyBase_IsParryable_AndParryForcesStagger()
|
||||||
|
{
|
||||||
|
// 接收端:EnemyBase 必须能被 Combat 层当作 IParryable 拿到
|
||||||
|
// (Combat 不能反向依赖 Enemies,所以接缝只能是接口)。
|
||||||
|
_attacker = new GameObject("enemy");
|
||||||
|
var enemy = _attacker.AddComponent<EnemyBase>();
|
||||||
|
|
||||||
|
Assert.IsInstanceOf<IParryable>(enemy, "EnemyBase 应实现 IParryable");
|
||||||
|
|
||||||
|
((IParryable)enemy).ReceiveParry(0.5f);
|
||||||
|
|
||||||
|
Assert.AreEqual(EnemyStateType.Stagger, enemy.CurrentState,
|
||||||
|
"被弹反应强制进入 Stagger——期间 IsControllable 为假,AI 决策自动让位");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9056219a9fc88404e8384fea384d6945
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -12,6 +12,20 @@ namespace BaseGames.Combat
|
|||||||
void TakeDamage(DamageInfo info);
|
void TakeDamage(DamageInfo info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 攻击被弹反时的反制承受方(攻击发起者实现,如 EnemyBase)。
|
||||||
|
///
|
||||||
|
/// 存在的理由是程序集方向:Combat 不能反向依赖 Enemies,而弹反成功的判定点
|
||||||
|
/// (<see cref="HurtBox.ReceiveDamage"/>)是唯一同时握有弹反结果与
|
||||||
|
/// <see cref="DamageInfo"/> 的地方,所以攻击者只能以接口形态经 DamageInfo 传进来。
|
||||||
|
/// 与 <see cref="IDamageable"/> 同款做法。
|
||||||
|
/// </summary>
|
||||||
|
public interface IParryable
|
||||||
|
{
|
||||||
|
/// <summary>本方发起的攻击被弹反:进入硬直 <paramref name="staggerDuration"/> 秒。</summary>
|
||||||
|
void ReceiveParry(float staggerDuration);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 可持有霸体的实体接口。HurtBox 在 ReceiveDamage 中做等级比较。
|
/// 可持有霸体的实体接口。HurtBox 在 ReceiveDamage 中做等级比较。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ namespace BaseGames.Combat
|
|||||||
/// [NonSerialized]:MonoBehaviour 引用不参与 Unity 资产序列化。
|
/// [NonSerialized]:MonoBehaviour 引用不参与 Unity 资产序列化。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[System.NonSerialized] public Projectile SourceProjectile;
|
[System.NonSerialized] public Projectile SourceProjectile;
|
||||||
|
/// <summary>
|
||||||
|
/// 发起这次攻击的一方(近战由 HitBox 在 Activate 时按宿主解析;无实现者时为 null)。
|
||||||
|
/// 用于弹反成功时调用 ReceiveParry 让攻击者硬直——与 <see cref="SourceProjectile"/> 同构:
|
||||||
|
/// 都是"攻击从哪来"的引用,供弹反分支反向作用于来源。
|
||||||
|
/// [NonSerialized]:MonoBehaviour 引用不参与 Unity 资产序列化。
|
||||||
|
/// </summary>
|
||||||
|
[System.NonSerialized] public IParryable Attacker;
|
||||||
|
|
||||||
// ── Builder ──────────────────────────────────────────────────────────
|
// ── Builder ──────────────────────────────────────────────────────────
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -68,6 +75,7 @@ namespace BaseGames.Combat
|
|||||||
private Vector2 _sourcePosition;
|
private Vector2 _sourcePosition;
|
||||||
private int _sourceLayer;
|
private int _sourceLayer;
|
||||||
private Projectile _sourceProjectile;
|
private Projectile _sourceProjectile;
|
||||||
|
private IParryable _attacker;
|
||||||
|
|
||||||
public Builder() { }
|
public Builder() { }
|
||||||
|
|
||||||
@@ -86,6 +94,7 @@ namespace BaseGames.Combat
|
|||||||
public Builder SetSourcePos(Vector2 v) { _sourcePosition = v; return this; }
|
public Builder SetSourcePos(Vector2 v) { _sourcePosition = v; return this; }
|
||||||
public Builder SetLayer(int v) { _sourceLayer = v; return this; }
|
public Builder SetLayer(int v) { _sourceLayer = v; return this; }
|
||||||
public Builder SetProjectile(Projectile v) { _sourceProjectile = v; return this; }
|
public Builder SetProjectile(Projectile v) { _sourceProjectile = v; return this; }
|
||||||
|
public Builder SetAttacker(IParryable v) { _attacker = v; return this; }
|
||||||
|
|
||||||
public DamageInfo Build() => new DamageInfo
|
public DamageInfo Build() => new DamageInfo
|
||||||
{
|
{
|
||||||
@@ -105,6 +114,7 @@ namespace BaseGames.Combat
|
|||||||
SourcePosition = _sourcePosition,
|
SourcePosition = _sourcePosition,
|
||||||
SourceLayer = _sourceLayer,
|
SourceLayer = _sourceLayer,
|
||||||
SourceProjectile = _sourceProjectile,
|
SourceProjectile = _sourceProjectile,
|
||||||
|
Attacker = _attacker,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +128,8 @@ namespace BaseGames.Combat
|
|||||||
Vector2 knockbackDir = default,
|
Vector2 knockbackDir = default,
|
||||||
Vector2 sourcePos = default,
|
Vector2 sourcePos = default,
|
||||||
int sourceLayer = 0,
|
int sourceLayer = 0,
|
||||||
Projectile sourceProjectile = null)
|
Projectile sourceProjectile = null,
|
||||||
|
IParryable attacker = null)
|
||||||
{
|
{
|
||||||
int baseAmt = Mathf.RoundToInt(so.BaseDamage * so.DamageMultiplier);
|
int baseAmt = Mathf.RoundToInt(so.BaseDamage * so.DamageMultiplier);
|
||||||
return new DamageInfo
|
return new DamageInfo
|
||||||
@@ -139,6 +150,7 @@ namespace BaseGames.Combat
|
|||||||
SourcePosition = sourcePos,
|
SourcePosition = sourcePos,
|
||||||
SourceLayer = sourceLayer,
|
SourceLayer = sourceLayer,
|
||||||
SourceProjectile = sourceProjectile,
|
SourceProjectile = sourceProjectile,
|
||||||
|
Attacker = attacker,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ namespace BaseGames.Combat
|
|||||||
// 宿主投射物缓存(Activate 时填入,DamageInfo.SourceProjectile 写入用)
|
// 宿主投射物缓存(Activate 时填入,DamageInfo.SourceProjectile 写入用)
|
||||||
private Projectile _ownerProjectile;
|
private Projectile _ownerProjectile;
|
||||||
|
|
||||||
|
// 宿主的弹反反制承受方(Activate 时填入,DamageInfo.Attacker 写入用)。
|
||||||
|
// 弹反成功时由 HurtBox 反向调用它的 ReceiveParry。
|
||||||
|
private IParryable _ownerParryable;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 激活 HitBox。source/attacker 均可选,未传则使用 Inspector 默认值。
|
/// 激活 HitBox。source/attacker 均可选,未传则使用 Inspector 默认值。
|
||||||
/// ⚠️ 不存在 Activate(float duration) 重载。
|
/// ⚠️ 不存在 Activate(float duration) 重载。
|
||||||
@@ -106,6 +110,9 @@ namespace BaseGames.Combat
|
|||||||
_attackerTransform = attacker ?? transform;
|
_attackerTransform = attacker ?? transform;
|
||||||
_isActive = true;
|
_isActive = true;
|
||||||
_ownerRigidbody = _attackerTransform.GetComponentInParent<Rigidbody2D>();
|
_ownerRigidbody = _attackerTransform.GetComponentInParent<Rigidbody2D>();
|
||||||
|
// 攻击者的弹反反制承受方;与 _ownerRigidbody 同源解析,供 DamageInfo.Attacker 写入。
|
||||||
|
// 放 Activate 而非 Awake:attacker 可由调用方传入,宿主要到这时才确定。
|
||||||
|
_ownerParryable = _attackerTransform.GetComponentInParent<IParryable>();
|
||||||
foreach (var col in _directColliders) col.enabled = true;
|
foreach (var col in _directColliders) col.enabled = true;
|
||||||
foreach (var proxy in _proxies) proxy.SetEnabled(true);
|
foreach (var proxy in _proxies) proxy.SetEnabled(true);
|
||||||
// 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标)
|
// 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标)
|
||||||
@@ -265,7 +272,8 @@ namespace BaseGames.Combat
|
|||||||
knockDir,
|
knockDir,
|
||||||
_attackerTransform.position,
|
_attackerTransform.position,
|
||||||
_attackerTransform.gameObject.layer,
|
_attackerTransform.gameObject.layer,
|
||||||
_ownerProjectile);
|
_ownerProjectile,
|
||||||
|
_ownerParryable);
|
||||||
info.HitActivationId = _currentActivationId;
|
info.HitActivationId = _currentActivationId;
|
||||||
|
|
||||||
// hitPoint:优先使用触发命中的碰撞体中心在目标表面的最近点;
|
// hitPoint:优先使用触发命中的碰撞体中心在目标表面的最近点;
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ namespace BaseGames.Combat
|
|||||||
// 若攻击来源是投射物,按弹反者阵营反射:
|
// 若攻击来源是投射物,按弹反者阵营反射:
|
||||||
// 玩家弹反翻转阵营 Layer 与伤害目标层;敌人弹反仅反转方向
|
// 玩家弹反翻转阵营 Layer 与伤害目标层;敌人弹反仅反转方向
|
||||||
info.SourceProjectile?.ReflectBy(transform);
|
info.SourceProjectile?.ReflectBy(transform);
|
||||||
|
// 近战来源:让发起这次攻击的一方硬直。时长取弹反配置里的权威值,
|
||||||
|
// 不在此另立常量。攻击者为空(陷阱 / 环境伤害等旁路)时自然跳过。
|
||||||
|
info.Attacker?.ReceiveParry(_parrySystem.StaggerDuration);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace BaseGames.Enemies
|
|||||||
/// ⚠️ _nav 字段类型为 IEnemyNavigator(在 BaseGames.Enemies.Navigation 中实现具体类)。
|
/// ⚠️ _nav 字段类型为 IEnemyNavigator(在 BaseGames.Enemies.Navigation 中实现具体类)。
|
||||||
/// 实现 IPoolable:配合 PooledObject 支持对象池复用,避免频繁 Destroy/Instantiate。
|
/// 实现 IPoolable:配合 PooledObject 支持对象池复用,避免频繁 Destroy/Instantiate。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EnemyBase : MonoBehaviour, IDamageable, IPoolable
|
public class EnemyBase : MonoBehaviour, IDamageable, IPoolable, IParryable
|
||||||
{
|
{
|
||||||
[Header("标识")]
|
[Header("标识")]
|
||||||
[SerializeField] private string _enemyId; // 任务系统 / Boss 进程追踪用,如 "Enemy_SpiderGuard"
|
[SerializeField] private string _enemyId; // 任务系统 / Boss 进程追踪用,如 "Enemy_SpiderGuard"
|
||||||
@@ -429,8 +429,8 @@ namespace BaseGames.Enemies
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 被弹反时调用:强制进入 Stagger 并在 staggerDuration 秒后恢复。
|
/// 被弹反时调用:强制进入 Stagger 并在 staggerDuration 秒后恢复。
|
||||||
/// Stagger 期间 IsControllable 为 false,AI 决策自动让位——不需要额外的 AI 信号。
|
/// Stagger 期间 IsControllable 为 false,AI 决策自动让位——不需要额外的 AI 信号。
|
||||||
/// ⚠️ 目前**零调用者**:弹反管线(HurtBox → ParrySystem.ConsumeParry)拿不到攻击者引用,
|
/// IParryable 实现:由 HurtBox 在弹反成功时经 DamageInfo.Attacker 调用,
|
||||||
/// 接线是一件独立任务(DamageInfo 加 attacker,或弹反成功时回调发起攻击的 HitBox)。
|
/// 时长取 ParryConfigSO.StaggerDuration。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void ReceiveParry(float staggerDuration = 0.5f)
|
public virtual void ReceiveParry(float staggerDuration = 0.5f)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ namespace BaseGames.Parry
|
|||||||
/// <summary>启用/禁用弹反输入(玩家能力解锁前设为 false)。</summary>
|
/// <summary>启用/禁用弹反输入(玩家能力解锁前设为 false)。</summary>
|
||||||
public bool IsEnabled { get; set; } = true;
|
public bool IsEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>被本方弹反的攻击者应硬直多久(秒)。由 HurtBox 在弹反成功时读取。</summary>
|
||||||
|
public float StaggerDuration => _config.StaggerDuration;
|
||||||
|
|
||||||
// ── C# 事件 ───────────────────────────────────────────────────────────
|
// ── C# 事件 ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user