引入属性、伤害管线

This commit is contained in:
2026-09-18 21:43:22 +08:00
parent 92e1ea0d55
commit ba25ec1926
108 changed files with 2920 additions and 720 deletions
@@ -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);
}
}
}