From 5cc93483d3a51a6a78e2b89894982a8b87c5c00b Mon Sep 17 00:00:00 2001 From: Joywayer Date: Thu, 30 Jul 2026 14:15:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(combat):=20HurtBox=20=E8=87=AA=E5=B8=A6?= =?UTF-8?q?=E5=8F=97=E5=87=BB=E4=BC=A4=E5=AE=B3=E5=80=8D=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 倍率归 HurtBox 自己(弱点部位就是一个倍率 > 1 的 HurtBox), 取代 WeakPointSystem 那份写进私有字段却零调用者的倍率—— 其注释称由 BossStats 调用,而项目里根本没有 BossStats 类型。 倍率 1 时数值原样返回而非"乘完再钳最低 1":本改动先于任何消费者落地, 若默认路径引入钳制会静默改变游戏里每一次伤害计算。 测试用 ApplyDamageMultiplier(0)==0 守住这条——10 在两种实现下都得 10, 只有 0 能把二者区分开。变异验证:去掉该捷径后恰好该断言变红。 --- Assets/Tests/EditMode/Combat.meta | 8 ++ .../EditMode/Combat/HurtBoxMultiplierTests.cs | 77 +++++++++++++++++++ .../Combat/HurtBoxMultiplierTests.cs.meta | 11 +++ Assets/_Game/Scripts/Combat/HurtBox.cs | 35 +++++++-- 4 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 Assets/Tests/EditMode/Combat.meta create mode 100644 Assets/Tests/EditMode/Combat/HurtBoxMultiplierTests.cs create mode 100644 Assets/Tests/EditMode/Combat/HurtBoxMultiplierTests.cs.meta diff --git a/Assets/Tests/EditMode/Combat.meta b/Assets/Tests/EditMode/Combat.meta new file mode 100644 index 00000000..525b8828 --- /dev/null +++ b/Assets/Tests/EditMode/Combat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e314cb3fe82112a409b7632389cdb35c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/EditMode/Combat/HurtBoxMultiplierTests.cs b/Assets/Tests/EditMode/Combat/HurtBoxMultiplierTests.cs new file mode 100644 index 00000000..259c5c54 --- /dev/null +++ b/Assets/Tests/EditMode/Combat/HurtBoxMultiplierTests.cs @@ -0,0 +1,77 @@ +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using BaseGames.Combat; + +namespace BaseGames.Tests.EditMode.Combat +{ + public class HurtBoxMultiplierTests + { + /// 测试期建出的宿主对象;在 TearDown 统一销毁,断言失败也不会残留到编辑器场景。 + private GameObject _host; + + [SetUp] + public void SetUp() => LogAssert.ignoreFailingMessages = true; + + [TearDown] + public void TearDown() + { + // 项目关闭了 Domain/Scene Reload:断言失败会跳过用例后续语句, + // 清理必须放在 TearDown,否则失败一次就往编辑器场景里漏一个对象。 + if (_host != null) Object.DestroyImmediate(_host); + _host = null; + LogAssert.ignoreFailingMessages = false; + } + + /// + /// HurtBox 标了 [RequireComponent(typeof(Collider2D))],而 Collider2D 是抽象类, + /// Unity 无法自动补齐——裸 AddComponent<HurtBox>() 会返回真 null。 + /// 必须先挂一个具体碰撞体。 + /// + private HurtBox CreateHurtBox() + { + _host = new GameObject("hurtbox"); + _host.AddComponent(); + var hb = _host.AddComponent(); + Assert.IsNotNull(hb, "HurtBox 需要先有具体 Collider2D 才能挂载"); + return hb; + } + + [Test] + public void DefaultMultiplier_IsOne() + { + var hb = CreateHurtBox(); + Assert.AreEqual(1f, hb.DamageMultiplier); + } + + [Test] + public void SetDamageMultiplier_ClampsToNonNegative() + { + var hb = CreateHurtBox(); + + hb.SetDamageMultiplier(2.5f); + Assert.AreEqual(2.5f, hb.DamageMultiplier); + + hb.SetDamageMultiplier(-3f); + Assert.AreEqual(0f, hb.DamageMultiplier, "负倍率无意义,钳到 0"); + } + + [Test] + public void ApplyMultiplier_ScalesRawAmount_AndFloorsAtOne() + { + var hb = CreateHurtBox(); + + hb.SetDamageMultiplier(2f); + Assert.AreEqual(20, hb.ApplyDamageMultiplier(10)); + + hb.SetDamageMultiplier(0f); + Assert.AreEqual(1, hb.ApplyDamageMultiplier(10), "倍率 0 也至少造成 1 点,与防御减免同规则"); + + hb.SetDamageMultiplier(1f); + Assert.AreEqual(10, hb.ApplyDamageMultiplier(10), "倍率 1 时数值原样通过"); + // 倍率 1 必须是"原样通过",而不是"乘完再钳最低 1":10 在两种实现下都得 10, + // 只有 0 能把二者区分开。这条断言守住"默认倍率不改变任何既有伤害数值"。 + Assert.AreEqual(0, hb.ApplyDamageMultiplier(0), "倍率 1 时原样通过,不引入最低 1 的钳制"); + } + } +} diff --git a/Assets/Tests/EditMode/Combat/HurtBoxMultiplierTests.cs.meta b/Assets/Tests/EditMode/Combat/HurtBoxMultiplierTests.cs.meta new file mode 100644 index 00000000..745164e5 --- /dev/null +++ b/Assets/Tests/EditMode/Combat/HurtBoxMultiplierTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: faac4f58895b7714b97a548a16181462 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Combat/HurtBox.cs b/Assets/_Game/Scripts/Combat/HurtBox.cs index 72dd7fa4..8b0e9b58 100644 --- a/Assets/_Game/Scripts/Combat/HurtBox.cs +++ b/Assets/_Game/Scripts/Combat/HurtBox.cs @@ -4,7 +4,7 @@ using BaseGames.Parry; namespace BaseGames.Combat { /// - /// 受击盒组件。实现完整 8 步伤害流水线(架构 06_CombatModule §5)。 + /// 受击盒组件。实现完整 9 步伤害流水线(架构 06_CombatModule §5)。 /// 挂载在角色根节点或指定子节点上,Collider2D 需设 IsTrigger = true, /// Layer = PlayerHurtBox 或 EnemyHurtBox。 /// @@ -33,6 +33,28 @@ namespace BaseGames.Combat public void SetPoiseSource(IPoiseSource src) => _poiseSource = src; public void SetInvincible(bool value) => _isHurtBoxInvincible = value; public void SetActive(bool value) => _isActive = value; + + // 本 HurtBox 专属的受击倍率(弱点部位 > 1;默认 1 = 无影响)。 + // 由能力在弱点窗口期间开关,语义与"这个部位更脆弱"一致。 + private float _damageMultiplier = 1f; + + /// 本 HurtBox 当前的受击伤害倍率。 + public float DamageMultiplier => _damageMultiplier; + + /// 设置受击倍率(弱点窗口开合时调用)。负值钳到 0。 + public void SetDamageMultiplier(float value) + => _damageMultiplier = UnityEngine.Mathf.Max(0f, value); + + /// + /// 按本 HurtBox 的倍率缩放伤害。最低 1 点,与防御减免同规则 + /// (倍率为 0 也不该让攻击完全无效——那属于无敌帧的语义,不是弱点倍率的)。 + /// 倍率为 1 时原样返回,保证默认状态下伤害数值与未引入倍率前完全一致。 + /// + public int ApplyDamageMultiplier(int raw) + => _damageMultiplier == 1f + ? raw + : UnityEngine.Mathf.Max(1, UnityEngine.Mathf.RoundToInt(raw * _damageMultiplier)); + #if UNITY_EDITOR // 付给编辑器的只读属性——避免反射并限制编辑器与运行时字段名耐合性。 public object EditorOwner => _owner; @@ -109,15 +131,18 @@ namespace BaseGames.Combat info.Amount = passThrough; } - // 5. 计算 FinalDamage(防御减免,最低 1) + // 5. 本 HurtBox 受击倍率(弱点部位;默认 1 时无影响) + info.Amount = ApplyDamageMultiplier(info.Amount); + + // 6. 计算 FinalDamage(防御减免,最低 1) int finalDamage = UnityEngine.Mathf.Max(1, info.Amount - _owner.Defense); info.Amount = finalDamage; info.FinalDamage = finalDamage; - // 6. 调用 _owner.TakeDamage + // 7. 调用 _owner.TakeDamage _owner.TakeDamage(info); - // 7. 全局广播 + // 8. 全局广播 _onDamageDealt?.Raise(info); _onHitConfirmed?.Raise(new HitInfo { @@ -125,7 +150,7 @@ namespace BaseGames.Combat HitPoint = resolvedHitPoint, }); - // 8. 状态效果触发(DoT — Fire / Poison) + // 9. 状态效果触发(DoT — Fire / Poison) // _statusEffectable 已在 Awake 中缓存,无需每次受击调用 GetComponent _statusEffectable?.ApplyStatusEffect(info.Type); }