コード例 #1
0
        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);
        }
コード例 #2
0
        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})") },
        };