引入属性、伤害管线
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user