private ValueBuilder CombineSource( IDamageRelatedStatBuilder statToCombine, Func <IDamageRelatedStatBuilder, IValueBuilder> handCombiner) => ValueFactory.If(_stat.SkillHitDamageSource.Value.Eq((int)DamageSource.Attack)) .Then(handCombiner(statToCombine)) .ElseIf(_stat.SkillHitDamageSource.Value.Eq((int)DamageSource.Spell)) .Then(statToCombine.With(DamageSource.Spell).Value) .ElseIf(_stat.SkillHitDamageSource.Value.Eq((int)DamageSource.Secondary)) .Then(statToCombine.With(DamageSource.Secondary).Value) .Else(0);
=> new DataDrivenMechanicCollection(_modifierBuilder, BuilderFactories) { // skill hit damage // - DPS { TotalOverride, MetaStats.SkillDpsWithHits, MetaStats.AverageHitDamage.Value * ValueFactory.If(Stat.HitRate.IsSet).Then(Stat.HitRate.Value) .Else(MetaStats.CastRate.Value * MetaStats.SkillNumberOfHitsPerCast.Value) },
private IValueBuilder EffectiveStunThresholdValue(IStatBuilder stunThresholdStat) { // If stun threshold is less than 25%, it is scaled up. // See https://pathofexile.gamepedia.com/Stun#Stun_threshold var stunThreshold = stunThresholdStat.Value; return(ValueFactory .If(stunThreshold >= 0.25).Then(stunThreshold) .Else(0.25 - 0.25 * (0.25 - stunThreshold) / (0.5 - stunThreshold))); }
protected ValueBuilder ChanceToHitValue( IStatBuilder accuracyStat, IStatBuilder evasionStat, IConditionBuilder isBlinded) { var accuracy = accuracyStat.Value; var evasion = evasionStat.Value; var blindMultiplier = ValueFactory.If(isBlinded).Then(0.5).Else(1); return(100 * blindMultiplier * 1.15 * accuracy / (accuracy + (evasion / 4).Select(d => Math.Pow(d, 0.8), v => $"{v}^0.8"))); }
protected ValueBuilder PhysicalDamageReductionFromArmour(IStatBuilder armour, ValueBuilder physicalDamage) { var armourValue = ValueFactory.If(armour.ChanceToDouble.Value >= 100) .Then(2 * armour.Value) .Else(armour.Value); return(100 * ValueFactory.If(armourValue.Eq(0)) .Then(0) .Else(armourValue / (armourValue + 10 * physicalDamage.Average))); }
=> new DataDrivenMechanicCollection(ModifierBuilder, BuilderFactories) { // skill hit damage // - DPS { TotalOverride, MetaStats.SkillDpsWithHits, MetaStats.AverageHitDamage.Value * ValueFactory.If(MetaStats.SkillDpsWithHitsCalculationMode.Value.Eq((double)DpsCalculationMode.HitRateBased)) .Then(Stat.HitRate.Value) .ElseIf(MetaStats.SkillDpsWithHitsCalculationMode.Value.Eq((double)DpsCalculationMode.CooldownBased)) .Then(1000 / Stat.Cooldown.Value * Stat.SkillNumberOfHitsPerCast.Value) .ElseIf(MetaStats.SkillDpsWithHitsCalculationMode.Value.Eq((double)DpsCalculationMode.AverageCast)) .Then(Stat.SkillNumberOfHitsPerCast.Value) .Else(MetaStats.CastRate.Value * Stat.SkillNumberOfHitsPerCast.Value) },
private ValueBuilder SkillUsesHandAsMultiplier(AttackDamageHand hand) => ValueFactory.If(_stat.SkillUsesHand(hand).IsSet).Then(1).Else(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})") }, };