317 lines
12 KiB
C#
317 lines
12 KiB
C#
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<SepCore.Components.HealthComponent>();
|
|
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<SepCore.Components.StatComponent>();
|
|
var moveComp = go.AddComponent<SepCore.Components.MovementComponent>();
|
|
var healthComp = go.AddComponent<SepCore.Components.HealthComponent>();
|
|
|
|
// 初始化:基础速度 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);
|
|
}
|
|
}
|
|
}
|