using NUnit.Framework; using SepCore.Battle; using SepCore.Definition; namespace VampireLike.Tests.Gameplay.EditMode { [TestFixture] public class DamagePipelineTests { [Test] public void DamageScaling_AddsMeleeDamageBeforePercentMultiplier() { var attackerPipeline = new AttributePipeline(); attackerPipeline.SetBaseValue(StatType.MeleeDamage, 20f); attackerPipeline.SetBaseValue(StatType.DamagePercent, 0.50f); // +50% var context = new DamageContext { BaseDamage = 100, DamageScalingStat = StatType.MeleeDamage, ScalingRatio = 1.0f, BaseCritChance = 0f, CritMultiplier = 1.5f, AttackerPipeline = attackerPipeline, DefenderPipeline = null, FallbackDefenderArmor = 0f, FallbackDefenderDodge = 0f }; // Raw = 100 + 20 = 120 // Amplified = 120 * (1 + 0.5) = 180 // Armor = 0 -> Mitigation = 1.0 -> 180 DamageResult result = DamagePipeline.Calculate(in context); Assert.AreEqual(180, result.Damage); Assert.IsFalse(result.IsDodge); Assert.IsFalse(result.IsCrit); } [Test] public void DamageScaling_RangedAndElemental_ApplyToRespectiveTypes() { var attackerPipeline = new AttributePipeline(); attackerPipeline.SetBaseValue(StatType.RangedDamage, 30f); attackerPipeline.SetBaseValue(StatType.ElementalDamage, 50f); // 1. 远程伤害加成 var rangedContext = new DamageContext { BaseDamage = 50, DamageScalingStat = StatType.RangedDamage, ScalingRatio = 1.0f, AttackerPipeline = attackerPipeline }; DamageResult rangedResult = DamagePipeline.Calculate(in rangedContext); Assert.AreEqual(80, rangedResult.Damage); // 50 + 30 = 80 // 2. 元素伤害加成(带 0.8 加成系数) var elementalContext = new DamageContext { BaseDamage = 50, DamageScalingStat = StatType.ElementalDamage, ScalingRatio = 0.8f, AttackerPipeline = attackerPipeline }; DamageResult elementalResult = DamagePipeline.Calculate(in elementalContext); Assert.AreEqual(90, elementalResult.Damage); // 50 + 50 * 0.8 = 90 } [Test] public void ArmorMitigation_PositiveArmor_AppliesHyperbolicReduction() { // 基础伤害 100,测试双曲公式 1 / (1 + Armor / 15) // 1. Armor = 15 -> 减伤 50% -> 50 var context15 = new DamageContext { BaseDamage = 100, FallbackDefenderArmor = 15f }; DamageResult result15 = DamagePipeline.Calculate(in context15); Assert.AreEqual(50, result15.Damage); // 2. Armor = 30 -> 减伤 66.7% -> 100 / 3 = 33.33 -> 向上取整 34 var context30 = new DamageContext { BaseDamage = 100, FallbackDefenderArmor = 30f }; DamageResult result30 = DamagePipeline.Calculate(in context30); Assert.AreEqual(34, result30.Damage); } [Test] public void ArmorMitigation_NegativeArmor_IncreasesDamageTaken() { // 基础伤害 100,负护甲增伤公式: 2 - 1 / (1 - Armor / 15) // Armor = -15 -> 2 - 1 / (1 - (-1)) = 2 - 0.5 = 1.5 -> 150 var context = new DamageContext { BaseDamage = 100, FallbackDefenderArmor = -15f }; DamageResult result = DamagePipeline.Calculate(in context); Assert.AreEqual(150, result.Damage); } [Test] public void Crit_WhenGuaranteed_MultipliesDamageCorrectly() { var context = new DamageContext { BaseDamage = 100, BaseCritChance = 1.0f, // 100% 暴击率(触发 Clamp 90% 上限,但若直接传入高暴击率测试倍率) CritMultiplier = 2.0f, FallbackDefenderArmor = 0f }; // 测试 100 次计算,由于暴击率最高 90%,我们设置 BaseCritChance = 0.9f 并循环验证暴击时的数值 var guaranteedContext = new DamageContext { BaseDamage = 100, BaseCritChance = 0.9f, CritMultiplier = 2.0f }; bool sawCrit = false; for (int i = 0; i < 50; i++) { DamageResult result = DamagePipeline.Calculate(in guaranteedContext); if (result.IsCrit) { Assert.AreEqual(200, result.Damage); sawCrit = true; break; } } Assert.IsTrue(sawCrit, "At 90% crit chance, at least one crit should occur across 50 trials."); } [Test] public void Dodge_WhenGuaranteed_ReturnsZeroDamageAndIsDodgeTrue() { var defenderPipeline = new AttributePipeline(); defenderPipeline.SetBaseValue(StatType.Dodge, 0.80f); // 80% 闪避率上限 var context = new DamageContext { BaseDamage = 100, DefenderPipeline = defenderPipeline }; bool sawDodge = false; for (int i = 0; i < 50; i++) { DamageResult result = DamagePipeline.Calculate(in context); if (result.IsDodge) { Assert.AreEqual(0, result.Damage); sawDodge = true; break; } } Assert.IsTrue(sawDodge, "At 80% dodge chance, at least one dodge should occur across 50 trials."); } [Test] public void MinDamage_EnforcesFloor_WhenMitigationReducesDamageTooLow() { var context = new DamageContext { BaseDamage = 1, FallbackDefenderArmor = 9999f }; DamageResult result = DamagePipeline.Calculate(in context); Assert.AreEqual(1, result.Damage); } [Test] public void LifeSteal_TriggersWhenDamageDealtAndRateSucceeds() { var attackerPipeline = new AttributePipeline(); attackerPipeline.SetBaseValue(StatType.LifeSteal, 0.99f); // 99% 吸血概率 var context = new DamageContext { BaseDamage = 50, AttackerPipeline = attackerPipeline }; bool sawLifeSteal = false; for (int i = 0; i < 20; i++) { DamageResult result = DamagePipeline.Calculate(in context); if (result.IsLifeSteal) { Assert.AreEqual(1, result.LifeStealAmount); sawLifeSteal = true; break; } } Assert.IsTrue(sawLifeSteal, "At 99% life steal rate, life steal should trigger."); } [Test] public void DamageTypeExtensions_MapsCorrectlyToStatType() { Assert.AreEqual(StatType.MeleeDamage, DamageType.Melee.ToScalingStat()); Assert.AreEqual(StatType.RangedDamage, DamageType.Ranged.ToScalingStat()); Assert.AreEqual(StatType.ElementalDamage, DamageType.Element.ToScalingStat()); Assert.AreEqual(StatType.None, DamageType.None.ToScalingStat()); } [Test] public void AttackSpeed_ScalesCooldownCorrectly() { float baseCooldown = 1.0f; // 1. +50% 攻速 -> 1.0 / (1 + 0.5) = 0.6667s float speedBonus1 = 0.5f; float factor1 = UnityEngine.Mathf.Max(0.2f, 1f + speedBonus1); float cd1 = UnityEngine.Mathf.Max(0.02f, baseCooldown / factor1); Assert.AreEqual(1f / 1.5f, cd1, 1e-4f); // 2. -50% 攻速 -> 1.0 / (1 - 0.5) = 2.0s float speedBonus2 = -0.5f; float factor2 = UnityEngine.Mathf.Max(0.2f, 1f + speedBonus2); float cd2 = UnityEngine.Mathf.Max(0.02f, baseCooldown / factor2); Assert.AreEqual(2.0f, cd2, 1e-4f); // 3. -80% 攻速(配置表下限)-> 1.0 / (1 - 0.8) = 5.0s float speedBonus3 = -0.8f; float factor3 = UnityEngine.Mathf.Max(0.2f, 1f + speedBonus3); float cd3 = UnityEngine.Mathf.Max(0.02f, baseCooldown / factor3); Assert.AreEqual(5.0f, cd3, 1e-4f); // 4. -95% 攻速(受 0.2 保护下限钳制)-> 1.0 / 0.2 = 5.0s float speedBonus4 = -0.95f; float factor4 = UnityEngine.Mathf.Max(0.2f, 1f + speedBonus4); float cd4 = UnityEngine.Mathf.Max(0.02f, baseCooldown / factor4); Assert.AreEqual(5.0f, cd4, 1e-4f); } [Test] public void RangeBonus_CalculatesAttackRangeCorrectly() { float baseRange = 5.0f; // 5m // 1. 增加 2m (direct units) float bonus1 = 2.0f; float eff1 = bonus1 >= 50f ? bonus1 / 1000f : bonus1; float range1 = UnityEngine.Mathf.Max(0.5f, baseRange + eff1); Assert.AreEqual(7.0f, range1, 1e-4f); // 2. 增加 1500 (millimeters) float bonus2 = 1500f; float eff2 = bonus2 >= 50f ? bonus2 / 1000f : bonus2; float range2 = UnityEngine.Mathf.Max(0.5f, baseRange + eff2); Assert.AreEqual(6.5f, range2, 1e-4f); } [Test] public void LifeStealICD_ThrottlesRepeatedTriggersWithin100ms() { var go = new UnityEngine.GameObject("TestHealthEntity"); var health = go.AddComponent(); health.OnInit(100); health.CurrentHealth = 50; // 第一次吸血成功 (t = 1.00s) bool first = health.TryTriggerLifeSteal(1, customTime: 1.00f); Assert.IsTrue(first); Assert.AreEqual(51, health.CurrentHealth); // 50ms 后再次触发,小于 0.1s ICD,应被节流拦截 bool second = health.TryTriggerLifeSteal(1, customTime: 1.05f); Assert.IsFalse(second); Assert.AreEqual(51, health.CurrentHealth); // 110ms 后触发 (t = 1.11s),超过 0.1s ICD,应成功 bool third = health.TryTriggerLifeSteal(1, customTime: 1.11f); Assert.IsTrue(third); Assert.AreEqual(52, health.CurrentHealth); UnityEngine.Object.DestroyImmediate(go); } [Test] public void MovementAndHealthComponents_EvaluateViaAttributePipeline() { var go = new UnityEngine.GameObject("TestEntity"); var statComp = go.AddComponent(); var moveComp = go.AddComponent(); var healthComp = go.AddComponent(); // 初始化:基础速度 5m/s,基础生命 100 moveComp.OnInit(5.0f, go.transform, statComp, false, 0.5f, 4); healthComp.OnInit(100, statComp); Assert.AreEqual(5.0f, moveComp.Speed, 1e-4f); Assert.AreEqual(100, healthComp.MaxHealth); // 添加 +20% 移速修饰器 statComp.AddModifier(StatType.MoveSpeed, new StatModifier(0.20f, ModifierType.PercentAdd)); Assert.AreEqual(6.0f, moveComp.Speed, 1e-4f); // 5 * 1.2 = 6.0 // 添加 +50 生命上限修饰器 statComp.AddModifier(StatType.MaxHp, new StatModifier(50f, ModifierType.Flat)); Assert.AreEqual(150, healthComp.MaxHealth); UnityEngine.Object.DestroyImmediate(go); } } }