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);
}