using System; using NUnit.Framework; using SepCore.Battle; using SepCore.Definition; namespace VampireLike.Tests.Gameplay.EditMode { [TestFixture] public class AttributePipelineTests { [Test] public void FormulaOrder_CalculatesFlatPercentAddAndPercentMultCorrectly() { var stat = new CharacterStat(StatType.MeleeDamage, baseValue: 10f); // Formula: ((Base + Sum(Flat)) * (1 + Sum(PercentAdd))) * Prod(PercentMult) // Flat: +5, +3 -> Sum = +8 -> Base + Flat = 18 stat.AddModifier(new StatModifier(5f, ModifierType.Flat)); stat.AddModifier(new StatModifier(3f, ModifierType.Flat)); // PercentAdd: +20% (0.2), +30% (0.3) -> Sum = +50% -> 1 + 0.5 = 1.5 stat.AddModifier(new StatModifier(0.2f, ModifierType.PercentAdd)); stat.AddModifier(new StatModifier(0.3f, ModifierType.PercentAdd)); // PercentMult: 1.2x, 1.5x -> Prod = 1.8 stat.AddModifier(new StatModifier(1.2f, ModifierType.PercentMult)); stat.AddModifier(new StatModifier(1.5f, ModifierType.PercentMult)); // Expected: 18 * 1.5 * 1.8 = 48.6 Assert.AreEqual(48.6f, stat.Value, 1e-4f); } [Test] public void DirtyFlag_CachesValueUntilModified() { var stat = new CharacterStat(StatType.MaxHp, baseValue: 100f); var mod = new StatModifier(20f, ModifierType.Flat); stat.AddModifier(mod); float firstRead = stat.Value; Assert.AreEqual(120f, firstRead, 1e-4f); // 读取多次,验证缓存值一致 float secondRead = stat.Value; Assert.AreEqual(120f, secondRead, 1e-4f); // 移除修饰器后脏标记生效,值重新计算 stat.RemoveModifier(mod); Assert.AreEqual(100f, stat.Value, 1e-4f); // 修改 BaseValue 后脏标记生效 stat.BaseValue = 150f; Assert.AreEqual(150f, stat.Value, 1e-4f); } [Test] public void RemoveAllModifiersFromSource_RemovesOnlyMatchingSource() { var pipeline = new AttributePipeline(); var sourceA = new object(); var sourceB = new object(); pipeline.SetBaseValue(StatType.MeleeDamage, 10f); pipeline.SetBaseValue(StatType.MoveSpeed, 5f); // sourceA 附加加成到近战伤害与移速 pipeline.AddModifier(StatType.MeleeDamage, new StatModifier(10f, ModifierType.Flat, sourceA)); pipeline.AddModifier(StatType.MoveSpeed, new StatModifier(0.2f, ModifierType.PercentAdd, sourceA)); // sourceB 附加加成到近战伤害 pipeline.AddModifier(StatType.MeleeDamage, new StatModifier(5f, ModifierType.Flat, sourceB)); Assert.AreEqual(25f, pipeline.GetValue(StatType.MeleeDamage), 1e-4f); Assert.AreEqual(6f, pipeline.GetValue(StatType.MoveSpeed), 1e-4f); // 清理 sourceA int removedCount = pipeline.RemoveAllModifiersFromSource(sourceA); Assert.AreEqual(2, removedCount); // 验证 sourceA 被清除,sourceB 依然生效 Assert.AreEqual(15f, pipeline.GetValue(StatType.MeleeDamage), 1e-4f); Assert.AreEqual(5f, pipeline.GetValue(StatType.MoveSpeed), 1e-4f); } [Test] public void StatConfig_ClampsMinAndMaxCorrectly() { // 闪避率硬上限 80% (0.8f) 来自配置表 var dodgeConfig = new StatConfig(StatType.Dodge, hasMin: true, minValue: 0f, hasMax: true, maxValue: 0.80f); var dodgeStat = new CharacterStat(StatType.Dodge, baseValue: 0.50f, dodgeConfig); dodgeStat.AddModifier(new StatModifier(0.40f, ModifierType.Flat)); Assert.AreEqual(0.80f, dodgeStat.Value, 1e-4f); // 移速保底 0.5f 来自配置表 var speedConfig = new StatConfig(StatType.MoveSpeed, hasMin: true, minValue: 0.5f); var speedStat = new CharacterStat(StatType.MoveSpeed, baseValue: 3f, speedConfig); speedStat.AddModifier(new StatModifier(-10f, ModifierType.Flat)); Assert.AreEqual(0.5f, speedStat.Value, 1e-4f); // 暴击率硬上限 90% (0.9f) 来自配置表 var critConfig = new StatConfig(StatType.CritChance, hasMin: true, minValue: 0f, hasMax: true, maxValue: 0.90f); var critStat = new CharacterStat(StatType.CritChance, baseValue: 0.50f, critConfig); critStat.AddModifier(new StatModifier(0.60f, ModifierType.Flat)); Assert.AreEqual(0.90f, critStat.Value, 1e-4f); } [Test] public void FromLuban_ConvertsFlatAndPercentModifiersCorrectly() { // 1. 固定值 (Flat): 比如近战伤害 +15 var flatConfig = new StatModifierConfig(StatType.MeleeDamage, 15f, ModifierType.Flat); var flatStatMod = StatModifier.FromLuban(flatConfig); Assert.AreEqual(ModifierType.Flat, flatStatMod.Type); Assert.AreEqual(15f, flatStatMod.Value, 1e-4f); // 2. 百分比累加 (PercentAdd): 比如伤害 +20% (0.2f) var percentConfig = new StatModifierConfig(StatType.DamagePercent, 0.2f, ModifierType.PercentAdd); var percentStatMod = StatModifier.FromLuban(percentConfig); Assert.AreEqual(ModifierType.PercentAdd, percentStatMod.Type); Assert.AreEqual(0.2f, percentStatMod.Value, 1e-4f); // 3. 独立乘区 (PercentMult): 比如伤害 1.5 倍 var multConfig = new StatModifierConfig(StatType.DamagePercent, 1.5f, ModifierType.PercentMult); var multStatMod = StatModifier.FromLuban(multConfig); Assert.AreEqual(ModifierType.PercentMult, multStatMod.Type); Assert.AreEqual(1.5f, multStatMod.Value, 1e-4f); } [Test] public void OnStatChanged_FiresOnModifications() { var pipeline = new AttributePipeline(); StatType changedStat = StatType.None; float changedValue = 0f; int eventCount = 0; pipeline.OnStatChanged += (type, val) => { changedStat = type; changedValue = val; eventCount++; }; // 设置基础值 pipeline.SetBaseValue(StatType.Armor, 10f); Assert.AreEqual(StatType.Armor, changedStat); Assert.AreEqual(10f, changedValue, 1e-4f); Assert.AreEqual(1, eventCount); // 添加修饰器 var mod = new StatModifier(5f, ModifierType.Flat); pipeline.AddModifier(StatType.Armor, mod); Assert.AreEqual(15f, changedValue, 1e-4f); Assert.AreEqual(2, eventCount); // 移除修饰器 pipeline.RemoveModifier(StatType.Armor, mod); Assert.AreEqual(10f, changedValue, 1e-4f); Assert.AreEqual(3, eventCount); } [Test] public void DynamicConfig_MarksDirtyAndUpdatesClamp() { var stat = new CharacterStat(StatType.Dodge, baseValue: 0.90f); Assert.AreEqual(0.90f, stat.Value, 1e-4f); // 未注入配置时不钳制 // 动态注入配置,硬上限 80% stat.Config = new StatConfig(StatType.Dodge, hasMax: true, maxValue: 0.80f); Assert.AreEqual(0.80f, stat.Value, 1e-4f); // 自动标记脏并重新钳制 } } }