public void SkillDpsWithHits() { var calculator = Calculator.Create(); var nodes = calculator.NodeRepository; calculator.NewBatchUpdate() .AddModifiers(_givenMods) .AddModifier(Build(_builderFactories.StatBuilders.CastRate.With(DamageSource.Attack)), Form.BaseSet, 2) .AddModifier(Build(_builderFactories.ActionBuilders.CriticalStrike.Chance), Form.BaseSet, 10) .AddModifier(Build(_builderFactories.DamageTypeBuilders.Physical.Penetration), Form.BaseAdd, 10) .AddModifier(Build(_builderFactories.DamageTypeBuilders.Physical.Damage.ChanceToDouble), Form.BaseAdd, 20) .AddModifier(Build(_metaStats.DamageBaseSetEffectiveness), Form.BaseSet, 2) .DoUpdate(); var chanceToHit = ChanceToHit(calculator); var actual = nodes .GetNode(BuildMainHandSkillSingle(_metaStats.EnemyResistanceAgainstNonCrits(DamageType.Physical))) .Value.Single(); var expectedEnemyResistance = 60 - 10; Assert.AreEqual(expectedEnemyResistance, actual); actual = nodes .GetNode(BuildMainHandSkillSingle(_metaStats.EnemyResistanceAgainstCrits(DamageType.Physical))) .Value.Single(); Assert.AreEqual(expectedEnemyResistance, actual); actual = nodes .GetNode( BuildMainHandSkillSingle(_metaStats.EffectiveDamageMultiplierWithNonCrits(DamageType.Physical))) .Value.Single(); var expectedEffectiveDamageMultiplierWithNonCrits = (1 - expectedEnemyResistance / 100d) * 1.2 * 1.75; Assert.AreEqual(expectedEffectiveDamageMultiplierWithNonCrits, actual); actual = nodes .GetNode(BuildMainHandSkillSingle(_metaStats.EffectiveDamageMultiplierWithCrits(DamageType.Physical))) .Value.Single(); var expectedEffectiveDamageMultiplierWithCrits = expectedEffectiveDamageMultiplierWithNonCrits * 1.5; Assert.AreEqual(expectedEffectiveDamageMultiplierWithCrits, actual); actual = nodes .GetNode(BuildMainHandSkillSingle(_metaStats.DamageWithNonCrits(DamageType.Physical))) .Value.Single(); var baseDamage = 5 * 2; var doubleDamageMultiplier = 1.2; var expectedDamageWithNonCrits = baseDamage * expectedEffectiveDamageMultiplierWithNonCrits * doubleDamageMultiplier; Assert.AreEqual(expectedDamageWithNonCrits, actual); actual = nodes .GetNode(BuildMainHandSkillSingle(_metaStats.DamageWithCrits(DamageType.Physical))) .Value.Single(); var expectedDamageWithCrits = baseDamage * expectedEffectiveDamageMultiplierWithCrits * doubleDamageMultiplier; Assert.AreEqual(expectedDamageWithCrits, actual); actual = nodes .GetNode(BuildMainHandSkillSingle(_builderFactories.StatBuilders.ChanceToHit)) .Value.Single(); Assert.AreEqual(chanceToHit * 100, actual, 1e-10); actual = nodes .GetNode(BuildMainHandSkillSingle(_metaStats.AverageDamagePerHit)) .Value.Single(); var effectiveCritChance = 0.1 * chanceToHit; var expectedAverageDamagePerHit = expectedDamageWithNonCrits * (1 - effectiveCritChance) + expectedDamageWithCrits * effectiveCritChance; Assert.AreEqual(expectedAverageDamagePerHit, actual); actual = nodes .GetNode(BuildMainHandSkillSingle(_metaStats.AverageDamage)) .Value.Single(); var expectedAverageDamage = expectedAverageDamagePerHit * chanceToHit; Assert.AreEqual(expectedAverageDamage, actual, 1e-10); actual = nodes .GetNode(Build(_metaStats.SkillDpsWithHits).Single()) .Value.Single(); var expectedSkillDpsWithHits = expectedAverageDamage * 2; Assert.AreEqual(expectedSkillDpsWithHits, actual, 1e-10); }
private DataDrivenMechanicCollection CreateCollection() => new DataDrivenMechanicCollection(_modifierBuilder, BuilderFactories) { // skill hit damage // - DPS { TotalOverride, _stat.SkillDpsWithHits, _stat.AverageHitDamage.Value *_stat.CastRate.Value }, // - average damage { TotalOverride, _stat.AverageHitDamage, CombineSource(_stat.AverageDamage.WithHits, CombineHandsByAverage) }, // - average damage per source { TotalOverride, _stat.AverageDamage.WithHits.With(AttackDamageHand.MainHand), _stat.AverageDamagePerHit.With(AttackDamageHand.MainHand).Value * Stat.ChanceToHit.With(AttackDamageHand.MainHand).Value.AsPercentage }, { TotalOverride, _stat.AverageDamage.WithHits.With(AttackDamageHand.OffHand), _stat.AverageDamagePerHit.With(AttackDamageHand.OffHand).Value * Stat.ChanceToHit.With(AttackDamageHand.OffHand).Value.AsPercentage }, { TotalOverride, _stat.AverageDamage.WithHits.With(DamageSource.Spell), _stat.AverageDamagePerHit.With(DamageSource.Spell).Value }, { TotalOverride, _stat.AverageDamage.WithHits.With(DamageSource.Secondary), _stat.AverageDamagePerHit.With(DamageSource.Secondary).Value }, // - average damage of a successful hit per source { TotalOverride, _stat.AverageDamagePerHit, _stat.DamageWithNonCrits().WithHits, _stat.DamageWithCrits().WithHits, _stat.EffectiveCritChance, (nonCritDamage, critDamage, critChance) => nonCritDamage.Value.Average * (1 - critChance.Value) + critDamage.Value.Average * critChance.Value }, // - crit/non-crit damage per source and type { TotalOverride, dt => _stat.DamageWithNonCrits(dt).WithHits, dt => _stat.Damage(dt).WithHits, dt => _stat.EffectiveDamageMultiplierWithNonCrits(dt).WithHits, (_, damage, mult) => damage.Value * mult.Value }, { TotalOverride, dt => _stat.DamageWithCrits(dt).WithHits, dt => _stat.Damage(dt).WithHits, dt => _stat.EffectiveDamageMultiplierWithCrits(dt).WithHits, (_, damage, mult) => damage.Value * mult.Value }, // - effective crit/non-crit damage multiplier per source and type { TotalOverride, dt => _stat.EffectiveDamageMultiplierWithNonCrits(dt).WithHits, dt => _stat.EnemyResistanceAgainstNonCrits(dt), dt => DamageTaken(dt).WithHits.For(Enemy), (_, resistance, damageTaken) => DamageTakenMultiplier(resistance, damageTaken) }, { TotalOverride, dt => _stat.EffectiveDamageMultiplierWithCrits(dt).WithHits, dt => _stat.EnemyResistanceAgainstCrits(dt), dt => DamageTaken(dt).WithHits.For(Enemy), _ => CriticalStrike.Multiplier.WithHits, (_, resistance, damageTaken, mult) => DamageTakenMultiplier(resistance, damageTaken) * mult.Value.AsPercentage }, // - enemy resistance against crit/non-crit hits per source and type { TotalOverride, dt => _stat.EnemyResistanceAgainstNonCrits(dt), dt => DamageTypeBuilders.From(dt).IgnoreResistanceWithNonCrits, dt => DamageTypeBuilders.From(dt).PenetrationWithNonCrits, (dt, ignoreResistance, penetration) => ValueFactory.If(ignoreResistance.IsSet).Then(0) .Else(DamageTypeBuilders.From(dt).Resistance.For(Enemy).Value - penetration.Value) }, { TotalOverride, dt => _stat.EnemyResistanceAgainstCrits(dt), dt => DamageTypeBuilders.From(dt).IgnoreResistanceWithCrits, dt => DamageTypeBuilders.From(dt).PenetrationWithCrits, (dt, ignoreResistance, penetration) => ValueFactory.If(ignoreResistance.Value.Eq(1)).Then(0) .Else(DamageTypeBuilders.From(dt).Resistance.For(Enemy).Value - penetration.Value) }, // skill damage over time // - DPS = average damage = non-crit damage { TotalOverride, _stat.SkillDpsWithDoTs, _stat.AverageDamage.WithSkills(DamageSource.OverTime).Value }, { TotalOverride, _stat.AverageDamage.WithSkills(DamageSource.OverTime), _stat.DamageWithNonCrits().WithSkills(DamageSource.OverTime).Value }, // - damage per type { TotalOverride, dt => _stat.DamageWithNonCrits(dt).WithSkills(DamageSource.OverTime), dt => _stat.Damage(dt).WithSkills(DamageSource.OverTime).Value * _stat.EffectiveDamageMultiplierWithNonCrits(dt).WithSkills(DamageSource.OverTime).Value }, // - effective damage multiplier per type { TotalOverride, dt => _stat.EffectiveDamageMultiplierWithNonCrits(dt).WithSkills(DamageSource.OverTime), dt => EnemyDamageTakenMultiplier(DamageTypeBuilders.From(dt), DamageTaken(dt).WithSkills(DamageSource.OverTime)) }, // ignite damage // - average damage { TotalOverride, _stat.AverageAilmentDamage(Common.Builders.Effects.Ailment.Ignite), CombineSource(_stat.AverageDamage.With(Ailment.Ignite), CombineHandsForAverageAilmentDamage(Common.Builders.Effects.Ailment.Ignite)) }, // - effective crit/non-crit damage multiplier per source and type { TotalOverride, dt => _stat.EffectiveDamageMultiplierWithNonCrits(dt).With(Ailment.Ignite), _ => Fire.Damage.Taken.With(Ailment.Ignite), (_, damageTaken) => EnemyDamageTakenMultiplier(Fire, damageTaken) }, { TotalOverride, dt => _stat.EffectiveDamageMultiplierWithCrits(dt).With(Ailment.Ignite), _ => Fire.Damage.Taken.With(Ailment.Ignite), _ => CriticalStrike.Multiplier.With(Ailment.Ignite), (_, damageTaken, mult) => EnemyDamageTakenMultiplier(Fire, damageTaken) * mult.Value.AsPercentage }, // bleed damage // - average damage { TotalOverride, _stat.AverageAilmentDamage(Common.Builders.Effects.Ailment.Bleed), CombineHandsForAverageAilmentDamage(Common.Builders.Effects.Ailment.Bleed) (_stat.AverageDamage.With(Ailment.Bleed)) }, // - effective crit/non-crit damage multiplier per source and type { TotalOverride, dt => _stat.EffectiveDamageMultiplierWithNonCrits(dt).With(Ailment.Bleed), _ => Physical.Damage.Taken.With(Ailment.Bleed), (_, damageTaken) => EnemyDamageTakenMultiplier(Physical, damageTaken) }, { TotalOverride, dt => _stat.EffectiveDamageMultiplierWithCrits(dt).With(Ailment.Bleed), _ => Physical.Damage.Taken.With(Ailment.Bleed), _ => CriticalStrike.Multiplier.With(Ailment.Bleed), (_, damageTaken, mult) => EnemyDamageTakenMultiplier(Physical, damageTaken) * mult.Value.AsPercentage }, // poison damage // - average damage { TotalOverride, _stat.AverageAilmentDamage(Common.Builders.Effects.Ailment.Poison), CombineSource(_stat.AverageDamage.With(Ailment.Poison), CombineHandsForAverageAilmentDamage(Common.Builders.Effects.Ailment.Poison)) }, // - effective crit/non-crit damage multiplier per source and type { TotalOverride, dt => _stat.EffectiveDamageMultiplierWithNonCrits(dt).With(Ailment.Poison), _ => Chaos.Damage.Taken.With(Ailment.Poison), (_, damageTaken) => EnemyDamageTakenMultiplier(Chaos, damageTaken) }, { TotalOverride, dt => _stat.EffectiveDamageMultiplierWithCrits(dt).With(Ailment.Poison), _ => Chaos.Damage.Taken.With(Ailment.Poison), _ => CriticalStrike.Multiplier.With(Ailment.Poison), (_, damageTaken, mult) => EnemyDamageTakenMultiplier(Chaos, damageTaken) * mult.Value.AsPercentage }, // shared ailment damage // - DPS { TotalOverride, _stat.AilmentDps, ailment => _stat.AverageAilmentDamage(ailment).Value * _stat.AilmentEffectiveInstances(ailment).Value }, // - lifetime damage of one instance { TotalOverride, _stat.AilmentInstanceLifetimeDamage, ailment => _stat.AverageAilmentDamage(ailment).Value *Ailment.From(ailment).Duration.Value }, // - average damage per source { TotalOverride, ailment => _stat.AverageDamage.With(Ailment.From(ailment)), ailment => _stat.DamageWithNonCrits().With(Ailment.From(ailment)), ailment => _stat.DamageWithCrits().With(Ailment.From(ailment)), _ => _stat.EffectiveCritChance, ailment => Ailment.From(ailment).Chance, ailment => _stat.AilmentChanceWithCrits(ailment), AverageAilmentDamageFromCritAndNonCrit }, // - crit/non-crit damage per source and type { TotalOverride, (a, dt) => _stat.DamageWithNonCrits(dt).With(Ailment.From(a)), (a, dt) => _stat.Damage(dt).With(Ailment.From(a)), (a, dt) => _stat.EffectiveDamageMultiplierWithNonCrits(dt).With(Ailment.From(a)), (damage, mult) => damage.Value * mult.Value }, { TotalOverride, (a, dt) => _stat.DamageWithCrits(dt).With(Ailment.From(a)), (a, dt) => _stat.Damage(dt).With(Ailment.From(a)), (a, dt) => _stat.EffectiveDamageMultiplierWithCrits(dt).With(Ailment.From(a)), (damage, mult) => damage.Value * mult.Value }, // speed { TotalOverride, _stat.CastRate, CombineSource(Stat.CastRate, CombineHandsByAverage) }, { TotalOverride, _stat.CastTime, _stat.CastRate.Value.Invert }, { PercentMore, Stat.MovementSpeed, ActionSpeedValueForPercentMore }, { PercentMore, Stat.CastRate, ActionSpeedValueForPercentMore, Not(Or(With(Keyword.Totem), With(Keyword.Trap), With(Keyword.Mine))) }, { PercentMore, Stat.Totem.Speed, ActionSpeedValueForPercentMore }, { PercentMore, Stat.Trap.Speed, ActionSpeedValueForPercentMore }, { PercentMore, Stat.Mine.Speed, ActionSpeedValueForPercentMore }, // resistances/damage reduction { BaseSet, _stat.ResistanceAgainstHits(DamageType.Physical), Physical.Resistance.Value }, { BaseAdd, _stat.ResistanceAgainstHits(DamageType.Physical), 100 * Armour.Value / (Armour.Value + 10 * Physical.Damage.WithSkills.With(AttackDamageHand.MainHand).For(Enemy).Value) }, { BaseSet, _stat.ResistanceAgainstHits(DamageType.Physical).Maximum, 90 }, { TotalOverride, _stat.ResistanceAgainstHits(DamageType.Lightning), Lightning.Resistance.Value }, { TotalOverride, _stat.ResistanceAgainstHits(DamageType.Cold), Cold.Resistance.Value }, { TotalOverride, _stat.ResistanceAgainstHits(DamageType.Fire), Fire.Resistance.Value }, { TotalOverride, _stat.ResistanceAgainstHits(DamageType.Chaos), Chaos.Resistance.Value }, // damage mitigation (1 - (1 - resistance / 100) * damage taken) { TotalOverride, _stat.MitigationAgainstHits, dt => 1 - DamageTakenMultiplier(_stat.ResistanceAgainstHits(dt), DamageTaken(dt).WithSkills(DamageSource.Secondary)) }, { TotalOverride, _stat.MitigationAgainstDoTs, dt => 1 - DamageTakenMultiplier(DamageTypeBuilders.From(dt).Resistance, DamageTaken(dt).WithSkills(DamageSource.OverTime)) }, // chance to hit/evade { BaseSet, Evasion.Chance, 100 - ChanceToHitValue(Stat.Accuracy.With(AttackDamageHand.MainHand).For(Enemy), Evasion, Buff.Blind.IsOn(Enemy)) }, { BaseSet, Stat.ChanceToHit.With(AttackDamageHand.MainHand), ChanceToHitValue(Stat.Accuracy.With(AttackDamageHand.MainHand), Evasion.For(Enemy), Buff.Blind.IsOn(Self)) }, { BaseSet, Stat.ChanceToHit.With(AttackDamageHand.OffHand), ChanceToHitValue(Stat.Accuracy.With(AttackDamageHand.OffHand), Evasion.For(Enemy), Buff.Blind.IsOn(Self)) }, // chance to avoid { TotalOverride, _stat.ChanceToAvoidMeleeAttacks, 100 - 100 * (FailureProbability(Evasion.ChanceAgainstMeleeAttacks) * FailureProbability(Stat.Dodge.AttackChance) * FailureProbability(Block.AttackChance)) }, { TotalOverride, _stat.ChanceToAvoidProjectileAttacks, 100 - 100 * (FailureProbability(Evasion.ChanceAgainstProjectileAttacks) * FailureProbability(Stat.Dodge.AttackChance) * FailureProbability(Block.AttackChance)) }, { TotalOverride, _stat.ChanceToAvoidSpells, 100 - 100 * (FailureProbability(Stat.Dodge.SpellChance) * FailureProbability(Block.SpellChance)) }, // crit { TotalOverride, _stat.EffectiveCritChance.With(AttackDamageHand.MainHand), CriticalStrike.Chance.With(AttackDamageHand.MainHand).Value.AsPercentage * Stat.ChanceToHit.With(AttackDamageHand.MainHand).Value.AsPercentage }, { TotalOverride, _stat.EffectiveCritChance.With(AttackDamageHand.OffHand), CriticalStrike.Chance.With(AttackDamageHand.OffHand).Value.AsPercentage * Stat.ChanceToHit.With(AttackDamageHand.OffHand).Value.AsPercentage }, { TotalOverride, _stat.EffectiveCritChance.With(DamageSource.Spell), CriticalStrike.Chance.With(DamageSource.Spell).Value.AsPercentage }, { TotalOverride, _stat.EffectiveCritChance.With(DamageSource.Secondary), CriticalStrike.Chance.With(DamageSource.Secondary).Value.AsPercentage }, // pools { BaseAdd, p => p.Regen, p => _stat.RegenTargetPoolValue(p.BuildPool()) * p.Regen.Percent.Value.AsPercentage }, { TotalOverride, _stat.EffectiveRegen, p => p.Regen.Value * p.RecoveryRate.Value }, { TotalOverride, _stat.EffectiveRecharge, p => p.Recharge.Value * p.RecoveryRate.Value }, { TotalOverride, _stat.RechargeStartDelay, p => 2 / p.Recharge.Start.Value }, { TotalOverride, _stat.EffectiveLeechRate, p => p.Leech.Rate.Value * p.RecoveryRate.Value }, { TotalOverride, _stat.AbsoluteLeechRate, p => _stat.LeechTargetPoolValue(p) * _stat.EffectiveLeechRate(p).Value.AsPercentage }, { TotalOverride, _stat.AbsoluteLeechRateLimit, p => _stat.LeechTargetPoolValue(p.BuildPool()) * p.Leech.RateLimit.Value.AsPercentage }, { TotalOverride, _stat.TimeToReachLeechRateLimit, p => p.Leech.RateLimit.Value / p.Leech.Rate.Value / _stat.CastRate.Value }, // flasks { PercentMore, Flask.LifeRecovery, Flask.Effect.Value * 100 }, { PercentMore, Flask.ManaRecovery, Flask.Effect.Value * 100 }, { PercentMore, Flask.LifeRecovery, Flask.RecoverySpeed.Value * 100 }, { PercentMore, Flask.ManaRecovery, Flask.RecoverySpeed.Value * 100 }, { PercentMore, Flask.Duration, (100 / Flask.RecoverySpeed.Value) - 100 }, // ailments { TotalOverride, _stat.AilmentDealtDamageType(Common.Builders.Effects.Ailment.Ignite), (int)DamageType.Fire }, { TotalOverride, _stat.AilmentDealtDamageType(Common.Builders.Effects.Ailment.Bleed), (int)DamageType.Physical }, { TotalOverride, _stat.AilmentDealtDamageType(Common.Builders.Effects.Ailment.Ignite), (int)DamageType.Chaos }, { TotalOverride, _stat.AilmentCombinedEffectiveChance, ailment => CombineSource(_stat.AilmentEffectiveChance(ailment), CombineHandsByAverage) }, { TotalOverride, _stat.AilmentEffectiveChance, ailment => Ailment.From(ailment).Chance, ailment => _stat.AilmentChanceWithCrits(ailment), _ => _stat.EffectiveCritChance, (ailment, ailmentChance, ailmentChanceWithCrits, critChance) => (ailmentChance.Value.AsPercentage * (1 - critChance.Value) + ailmentChanceWithCrits.Value.AsPercentage * critChance.Value) * (1 - Ailment.From(ailment).Avoidance.For(Enemy).Value.AsPercentage) }, { TotalOverride, _stat.AilmentChanceWithCrits, ailment => Ailment.From(ailment).Chance, (ailment, ailmentChance) => ValueFactory .If(Ailment.From(ailment).CriticalStrikesAlwaysInflict.IsSet).Then(100) .Else(ailmentChance.Value) }, { TotalOverride, Ailment.Chill.On(Self), 1, Ailment.Freeze.IsOn(Self) }, { PercentIncrease, Ailment.Shock.AddStat(Damage.Taken), _stat.IncreasedDamageTakenFromShocks.Value }, { BaseSet, _stat.IncreasedDamageTakenFromShocks, 20 }, { TotalOverride, _stat.IncreasedDamageTakenFromShocks.Maximum, 50 }, { TotalOverride, _stat.IncreasedDamageTakenFromShocks.Minimum, 1 }, { PercentReduce, Ailment.Chill.AddStat(Stat.ActionSpeed), _stat.ReducedActionSpeedFromChill.Value }, { BaseSet, _stat.ReducedActionSpeedFromChill, 10 }, { TotalOverride, _stat.ReducedActionSpeedFromChill.Maximum, 30 }, { TotalOverride, _stat.ReducedActionSpeedFromChill.Minimum, 1 }, // - AilmentEffectiveInstances { TotalOverride, _stat.AilmentEffectiveInstances(Common.Builders.Effects.Ailment.Ignite), Ailment.Ignite.InstancesOn(Enemy).Maximum.Value }, { TotalOverride, _stat.AilmentEffectiveInstances(Common.Builders.Effects.Ailment.Bleed), Ailment.Bleed.InstancesOn(Enemy).Maximum.Value }, { TotalOverride, _stat.AilmentEffectiveInstances(Common.Builders.Effects.Ailment.Poison), Ailment.Poison.Duration.Value *_stat.CastRate.Value * CombineSource(_stat.AilmentEffectiveChance(Common.Builders.Effects.Ailment.Poison), s => CombineByWeightedAverage( s.With(AttackDamageHand.MainHand).Value * Stat.ChanceToHit.With(AttackDamageHand.MainHand).Value.AsPercentage, SkillUsesHandAsMultiplier(AttackDamageHand.MainHand), s.With(AttackDamageHand.OffHand).Value * Stat.ChanceToHit.With(AttackDamageHand.OffHand).Value.AsPercentage, SkillUsesHandAsMultiplier(AttackDamageHand.OffHand))) }, // stun (see https://pathofexile.gamepedia.com/Stun) { PercentLess, Effect.Stun.Duration, Effect.Stun.Recovery.For(Enemy).Value * 100 }, { TotalOverride, _stat.EffectiveStunThreshold, Effect.Stun.Threshold, EffectiveStunThresholdValue }, { BaseSet, Effect.Stun.Chance, _stat.AverageDamage.WithHits, _stat.EffectiveStunThreshold, (damage, threshold) => 200 * damage.Value / (Life.For(Enemy).ValueFor(NodeType.Subtotal) * threshold.Value) }, { TotalOverride, _stat.StunAvoidanceWhileCasting, 1 - (1 - Effect.Stun.Avoidance.Value) * (1 - Effect.Stun.ChanceToAvoidInterruptionWhileCasting.Value) }, // radius { PercentMore, Stat.Radius, Stat.AreaOfEffect.Value.Select(Math.Sqrt, v => $"Sqrt({v})") }, };