public float GetOHUptime() { if (offHandEnchant != null && offHandEnchant.Trigger == Trigger.SpellHit) { return(offHandEnchant.GetAverageUptime(1f / _cs.GetSpellAttacksPerSec(), _cs.ChanceSpellHit, _cs.UnhastedOHSpeed, _cs.FightLength)); } return(offHandEnchant == null ? 0f : offHandEnchant.GetAverageUptime(_cs.HastedOHSpeed, _cs.ChanceWhiteHitOH, _cs.UnhastedOHSpeed, _cs.FightLength)); }
public void GetAverageUptimeTest() { // test interpolation at proc chance = 100% Properties.GeneralSettings.Default.ProcEffectMode = 3; SpecialEffect.UpdateCalculationMode(); SpecialEffect target = new SpecialEffect(Trigger.PhysicalCrit, new Stats() { AttackPower = 632 }, 10.0f, 45.0f, 1.0f); // TODO: Initialize to an appropriate value float triggerInterval = 1.39778626F; float triggerChance = 1.0F; float attackSpeed = 3.4F; float fightDuration = 300.0F; float expected = 70.0F / 300.0F; float actual; actual = target.GetAverageUptime(triggerInterval, triggerChance, attackSpeed, fightDuration); Assert.AreEqual(expected, actual); }
private Stats CalcNormalProc(SpecialEffect effect, Dictionary <int, float> periods, Dictionary <int, float> chances) { Stats effectStats = effect.Stats; Stats proc = effect.GetAverageStats(periods[(int)effect.Trigger], chances[(int)effect.Trigger], CalculationsWarlock.AVG_UNHASTED_CAST_TIME, BossOpts.BerserkTimer); // Handle "recursive effects" - i.e. those that *enable* a // proc during a short window. if (effect.Stats._rawSpecialEffectDataSize == 1 && periods.ContainsKey((int)effect.Stats._rawSpecialEffectData[0].Trigger)) { SpecialEffect inner = effect.Stats._rawSpecialEffectData[0]; Stats innerStats = inner.GetAverageStats(periods[(int)inner.Trigger], chances[(int)inner.Trigger], 1f, effect.Duration); float upTime = effect.GetAverageUptime(periods[(int)effect.Trigger], chances[(int)effect.Trigger], 1f, BossOpts.BerserkTimer); proc.Accumulate(innerStats, upTime); } // E.g. Sorrowsong if (effect.LimitedToExecutePhase) { proc *= CalcOpts.ThirtyFive; } return(proc); }
public float GetPactProcBenefit() { float pact = .02f * Mommy.Talents.DemonicPact; if (pact == 0) { return(0f); } float buff = StatUtils.GetBuffEffect( Mommy.Character.ActiveBuffs, Mommy.CalcSpellPower() * pact, "Spell Power", s => s.SpellPower); if (buff == 0) { return(0f); } SpecialEffect pactEffect = new SpecialEffect(0, null, 45f, 20f); float meleeRate; if (BaseMeleeDamage == 0) { meleeRate = 0f; } else { meleeRate = 1 / CalcMeleeSpeed(); } float spellRate; if (SpecialDamagePerSpellPower == 0) { spellRate = 0f; float specialSpeed = GetSpecialSpeed(); if (specialSpeed > 0) { meleeRate += 1 / GetSpecialSpeed(); } } else { spellRate = 1 / GetSpecialSpeed(); } float triggerRate = 1 / (meleeRate + spellRate); float uprate = pactEffect.GetAverageUptime( triggerRate, Utilities.GetWeightedSum( Mommy.HitChance * CalcSpellCrit(), spellRate, CalcMeleeCrit(), meleeRate), triggerRate, Mommy.Options.Duration); return(uprate * buff); }
private Stats CalcNormalProc(SpecialEffect effect, Dictionary<int, float> periods, Dictionary<int, float> chances) { Stats effectStats = effect.Stats; Stats proc = effect.GetAverageStats(periods[(int)effect.Trigger], chances[(int)effect.Trigger], CalculationsWarlock.AVG_UNHASTED_CAST_TIME, BossOpts.BerserkTimer); // Handle "recursive effects" - i.e. those that *enable* a // proc during a short window. if (effect.Stats._rawSpecialEffectDataSize == 1 && periods.ContainsKey((int)effect.Stats._rawSpecialEffectData[0].Trigger)) { SpecialEffect inner = effect.Stats._rawSpecialEffectData[0]; Stats innerStats = inner.GetAverageStats(periods[(int)inner.Trigger], chances[(int)inner.Trigger], 1f, effect.Duration); float upTime = effect.GetAverageUptime(periods[(int)effect.Trigger], chances[(int)effect.Trigger], 1f, BossOpts.BerserkTimer); proc.Accumulate(innerStats, upTime); } // E.g. Sorrowsong if (effect.LimitedToExecutePhase) { proc *= CalcOpts.ThirtyFive; } return proc; }
public float CalculateCastTime(CastingState castingState, float interruptProtection, float critRate, bool pom, float baseCastTime, out float channelReduction) { CalculationOptionsMage calculationOptions = castingState.CalculationOptions; float castingSpeed = castingState.CastingSpeed; float spellHasteRating = castingState.SpellHasteRating; float levelScalingFactor = calculationOptions.LevelScalingFactor; float hasteFactor = levelScalingFactor / 1000f; float rootCastingSpeed = castingSpeed / (1 + spellHasteRating * hasteFactor); float InterruptFactor = 0f; float maxPushback = 0f; if (calculationOptions.InterruptFrequency > 0f) { // interrupt factors of more than once per spell are not supported, so put a limit on it (up to twice is probably approximately correct) InterruptFactor = Math.Min(calculationOptions.InterruptFrequency, 2 * castingSpeed / baseCastTime); if (castingState.IcyVeins) { interruptProtection = 1; } maxPushback = 0.5f * Math.Max(0, 1 - interruptProtection); if (Channeled) { maxPushback = 0.0f; } } if (pom) { baseCastTime = 0.0f; } float globalCooldown = Math.Max(Spell.GlobalCooldownLimit, 1.5f / castingSpeed); float latency; if (baseCastTime <= 1.5f || Instant) { latency = calculationOptions.LatencyGCD; } else if (Channeled) { latency = calculationOptions.LatencyChannel; } else { latency = calculationOptions.LatencyCast; } float averageTicks = Ticks; float castTime = baseCastTime / castingSpeed; if (calculationOptions.Beta && Channeled) { float tickCastTime = castTime / Ticks; averageTicks = (float)Math.Floor(baseCastTime / tickCastTime); castTime = averageTicks * tickCastTime; } if (InterruptFactor > 0) { castTime = castTime * (1 + InterruptFactor * maxPushback) - (maxPushback * 0.5f) * maxPushback * InterruptFactor; } castTime += latency; float gcdcap = globalCooldown + calculationOptions.LatencyGCD; if (castTime < gcdcap) { castTime = gcdcap; } channelReduction = 0.0f; if (!calculationOptions.AdvancedHasteProcs) { for (int i = 0; i < castingState.Solver.HasteRatingEffectsCount; i++) { SpecialEffect effect = castingState.Solver.HasteRatingEffects[i]; float procs = 0.0f; int triggers = 0; switch (effect.Trigger) { case Trigger.DamageSpellCast: case Trigger.SpellCast: procs = CastProcs; triggers = (int)procs; break; case Trigger.DamageSpellCrit: case Trigger.SpellCrit: procs = critRate * averageTicks; triggers = (int)averageTicks; break; case Trigger.DamageSpellHit: case Trigger.SpellHit: procs = HitRate * averageTicks; triggers = (int)averageTicks; break; } if (procs == 0.0f) { continue; } float procHaste = effect.Stats.HasteRating; if (effect.Cooldown >= effect.Duration) { // hasted casttime float speed = rootCastingSpeed * (1 + (spellHasteRating + procHaste) * hasteFactor); float gcd = Math.Max(Spell.GlobalCooldownLimit, 1.5f / speed); float cast = baseCastTime / speed; if (calculationOptions.Beta && Channeled) { float tickCastTime = cast / Ticks; cast = (float)Math.Floor(baseCastTime / tickCastTime) * tickCastTime; } if (InterruptFactor > 0) { cast = cast * (1 + InterruptFactor * maxPushback) - (maxPushback * 0.5f) * maxPushback * InterruptFactor; } cast += latency; gcdcap = gcd + calculationOptions.LatencyGCD; if (cast < gcdcap) { cast = gcdcap; } float castsAffected = 0; if (triggers > 1) { // multi tick spell (i.e. AM) for (int c = 0; c < triggers; c++) { castsAffected += (float)Math.Ceiling((effect.Duration - c * castTime / triggers) / cast); } castsAffected /= triggers; } else { // single tick spell castsAffected = (float)Math.Ceiling(effect.Duration / cast); // should the first proc be already hasted? } float effectiveDuration = castsAffected * cast; // this isn't completely accurate, we should have made a separate SpecialEffect and change the actual duration // but performance would hurt so this'll have to do spellHasteRating += procHaste * (effectiveDuration / effect.Duration) * effect.GetAverageUptime(castTime / triggers, procs / triggers, 3.0f, calculationOptions.FightDuration); //spellHasteRating += procHaste * castsAffected * cast / (effect.Cooldown + castTime / procs / effect.Chance); //Haste += castingState.BasicStats.SpellHasteFor6SecOnCast_15_45 * 6f / (45f + CastTime / CastProcs / 0.15f); castingSpeed = rootCastingSpeed * (1 + spellHasteRating * hasteFactor); globalCooldown = Math.Max(Spell.GlobalCooldownLimit, 1.5f / castingSpeed); castTime = baseCastTime / castingSpeed; if (calculationOptions.Beta && Channeled) { float tickCastTime = castTime / Ticks; averageTicks = (float)Math.Floor(baseCastTime / tickCastTime); castTime = averageTicks * tickCastTime; } if (InterruptFactor > 0) { castTime = castTime * (1 + InterruptFactor * maxPushback) - (maxPushback * 0.5f) * maxPushback * InterruptFactor; } castTime += latency; gcdcap = globalCooldown + calculationOptions.LatencyGCD; if (castTime < gcdcap) { castTime = gcdcap; } } else if (effect.Cooldown == 0 && (effect.Trigger == Trigger.SpellCrit || effect.Trigger == Trigger.DamageSpellCrit)) { float rawHaste = spellHasteRating; castingSpeed /= (1 + spellHasteRating / 1000f * levelScalingFactor); float proccedSpeed = castingSpeed * (1 + (rawHaste + procHaste) / 1000f * levelScalingFactor); float proccedGcd = Math.Max(Spell.GlobalCooldownLimit, 1.5f / proccedSpeed); float proccedCastTime = baseCastTime / proccedSpeed; float proccedTicks = averageTicks; if (calculationOptions.Beta && Channeled) { float tickCastTime = proccedCastTime / Ticks; proccedTicks = (float)Math.Floor(baseCastTime / tickCastTime); castTime = proccedTicks * tickCastTime; } if (InterruptFactor > 0) { proccedCastTime = proccedCastTime * (1 + InterruptFactor * maxPushback) - (maxPushback * 0.5f) * maxPushback * InterruptFactor; } proccedCastTime += latency; if (proccedCastTime < proccedGcd + calculationOptions.LatencyGCD) { proccedCastTime = proccedGcd + calculationOptions.LatencyGCD; } int chancesToProc = (int)(((int)Math.Floor(effect.Duration / proccedCastTime) + 1) * proccedTicks); if (!(Instant || pom)) { chancesToProc -= 1; } if (AreaEffect) { chancesToProc *= calculationOptions.AoeTargets; } spellHasteRating = rawHaste + procHaste * (1 - (float)Math.Pow(1 - effect.Chance * critRate, chancesToProc)); //Haste = rawHaste + castingState.BasicStats.SpellHasteFor5SecOnCrit_50 * ProcBuffUp(1 - (float)Math.Pow(1 - 0.5f * CritRate, HitProcs), 5, CastTime); castingSpeed *= (1 + spellHasteRating / 1000f * levelScalingFactor); globalCooldown = Math.Max(Spell.GlobalCooldownLimit, 1.5f / castingSpeed); castTime = baseCastTime / castingSpeed; if (calculationOptions.Beta && Channeled) { float tickCastTime = castTime / Ticks; averageTicks = (float)Math.Floor(baseCastTime / tickCastTime); castTime = averageTicks * tickCastTime; } if (InterruptFactor > 0) { castTime = castTime * (1 + InterruptFactor * maxPushback) - (maxPushback * 0.5f + latency) * maxPushback * InterruptFactor; } castTime += latency; if (castTime < globalCooldown + calculationOptions.LatencyGCD) { castTime = globalCooldown + calculationOptions.LatencyGCD; } } } // on use stacking items for (int i = 0; i < castingState.Solver.StackingHasteEffectCooldownsCount; i++) { EffectCooldown effectCooldown = castingState.Solver.StackingHasteEffectCooldowns[i]; if (castingState.EffectsActive(effectCooldown.Mask)) { Stats stats = effectCooldown.SpecialEffect.Stats; for (int j = 0; j < stats._rawSpecialEffectDataSize; j++) { SpecialEffect effect = stats._rawSpecialEffectData[j]; float procHaste = effect.Stats.HasteRating; if (procHaste > 0) { float procs = 0.0f; switch (effect.Trigger) { case Trigger.DamageSpellCast: case Trigger.SpellCast: procs = CastProcs; break; case Trigger.DamageSpellCrit: case Trigger.SpellCrit: procs = critRate * averageTicks; break; case Trigger.DamageSpellHit: case Trigger.SpellHit: procs = HitRate * averageTicks; break; } if (procs == 0.0f) { continue; } // until they put in some good trinkets with such effects just do a quick dirty calculation float effectHasteRating; if (procs > averageTicks) { // some 100% on cast procs, happens because AM has 6 cast procs and only 5 ticks effectHasteRating = effect.GetAverageStackSize(castTime / procs, 1.0f, 3.0f, effectCooldown.SpecialEffect.Duration) * procHaste; } else { effectHasteRating = effect.GetAverageStackSize(castTime / averageTicks, procs / averageTicks, 3.0f, effectCooldown.SpecialEffect.Duration) * procHaste; } castingSpeed /= (1 + spellHasteRating / 1000f * levelScalingFactor); spellHasteRating += effectHasteRating; castingSpeed *= (1 + spellHasteRating / 1000f * levelScalingFactor); globalCooldown = Math.Max(Spell.GlobalCooldownLimit, 1.5f / castingSpeed); castTime = baseCastTime / castingSpeed; if (calculationOptions.Beta && Channeled) { float tickCastTime = castTime / Ticks; averageTicks = (float)Math.Floor(baseCastTime / tickCastTime); castTime = averageTicks * tickCastTime; } if (InterruptFactor > 0) { castTime = castTime * (1 + InterruptFactor * maxPushback) - (maxPushback * 0.5f) * maxPushback * InterruptFactor; } castTime += latency; if (castTime < globalCooldown + calculationOptions.LatencyGCD) { castTime = globalCooldown + calculationOptions.LatencyGCD; } } } } } } if (Channeled && calculationOptions.Beta) { channelReduction = 1 - averageTicks / Ticks; } // channeled pushback if (Channeled && InterruptFactor > 0) { int maxLostTicks = (int)Math.Ceiling(averageTicks * 0.25f * Math.Max(0, 1 - interruptProtection)); // pushbacks that happen up to pushbackCastTime cut the cast time to pushbackCastTime // pushbacks that happen after just terminate the channel // [---|---X---|---|---] castTime -= latency; float tickFactor = 0; for (int i = 0; i < maxLostTicks; i++) { tickFactor += InterruptFactor * castTime / averageTicks * (i + 1) / averageTicks; } tickFactor += InterruptFactor * (averageTicks - maxLostTicks) * castTime / averageTicks * maxLostTicks / averageTicks; if (calculationOptions.Beta) { channelReduction = 1 - averageTicks * (1 - tickFactor) / Ticks; } else { channelReduction = tickFactor; } castTime = castTime * (1 - tickFactor) + latency; } return(castTime); }
private float ApplySpecialEffect(SpecialEffect effect, DPSWarrCharacter charStruct, Dictionary<Trigger, float> triggerIntervals, Dictionary<Trigger, float> triggerChances, ref Base.StatsWarrior applyTo) { #if DEBUG //ConstructionCounts["ApplySpecialEffect"]++; #endif float fightDuration = charStruct.BossOpts.BerserkTimer; float fightDuration2Pass = charStruct.CalcOpts.SE_UseDur ? fightDuration : 0; Stats effectStats = effect.Stats; float upTime = 0f; //float avgStack = 1f; /*if (effect.Stats.TargetArmorReduction > 0f || effect.Stats.ArmorPenetrationRating > 0f) { //int j = 0; }*/ if (effect.Trigger == Trigger.Use) { if (effect.Stats._rawSpecialEffectDataSize == 1) { upTime = effect.GetAverageUptime(0f, 1f, charStruct.CombatFactors.CMHItemSpeed, fightDuration2Pass); //float uptime = (effect.Cooldown / fightDuration); List<SpecialEffect> nestedEffect = new List<SpecialEffect>(); nestedEffect.Add(effect.Stats._rawSpecialEffectData[0]); Base.StatsWarrior _stats2 = new Base.StatsWarrior(); ApplySpecialEffect(effect.Stats._rawSpecialEffectData[0], charStruct, triggerIntervals, triggerChances, ref _stats2); effectStats = _stats2; } else { upTime = effect.GetAverageStackSize(0f, 1f, charStruct.CombatFactors.CMHItemSpeed, fightDuration2Pass); } } else if (effect.Duration == 0f && triggerIntervals.ContainsKey(effect.Trigger) && !float.IsInfinity(triggerIntervals[effect.Trigger])) { upTime = effect.GetAverageProcsPerSecond(triggerIntervals[effect.Trigger], triggerChances[effect.Trigger], charStruct.CombatFactors.CMHItemSpeed, fightDuration2Pass); } else if (effect.Trigger == Trigger.ExecuteHit) { upTime = effect.GetAverageStackSize(triggerIntervals[effect.Trigger], triggerChances[effect.Trigger], charStruct.CombatFactors.CMHItemSpeed, fightDuration2Pass * (float)charStruct.BossOpts.Under20Perc); } else if (triggerIntervals.ContainsKey(effect.Trigger) && !float.IsInfinity(triggerIntervals[effect.Trigger])) { upTime = effect.GetAverageStackSize(triggerIntervals[effect.Trigger], triggerChances[effect.Trigger], charStruct.CombatFactors.CMHItemSpeed, fightDuration2Pass); } if (upTime > 0f) { if (effect.Duration == 0f) { applyTo.Accumulate(effectStats, upTime * fightDuration); } else if (upTime <= effect.MaxStack) { applyTo.Accumulate(effectStats, upTime); } return upTime; } return 0f; }
public void Solve(Character character, ref CharacterCalculationsMoonkin calcs) { CalculationOptionsMoonkin calcOpts = character.CalculationOptions as CalculationOptionsMoonkin; DruidTalents talents = character.DruidTalents; procEffects = new List <ProcEffect>(); RecreateSpells(talents, ref calcs); cachedResults = new Dictionary <string, RotationData>(); float trinketDPS = 0.0f; float baseSpellPower = calcs.SpellPower; float baseHit = GetSpellHit(calcs); float baseCrit = calcs.SpellCrit; float baseHaste = calcs.SpellHaste; BuildProcList(calcs); float maxDamageDone = 0.0f; float maxBurstDamageDone = 0.0f; SpellRotation maxBurstRotation = rotations[0]; SpellRotation maxRotation = rotations[0]; float manaPool = GetEffectiveManaPool(character, calcOpts, calcs); // Do tree calculations: Calculate damage per cast. float treeDamage = (talents.ForceOfNature == 1) ? DoTreeCalcs(baseSpellPower, calcs.BasicStats.PhysicalHaste, calcs.BasicStats.ArmorPenetration, calcs.BasicStats.PhysicalCrit, calcOpts.TreantLifespan, character.DruidTalents.Brambles) : 0.0f; // Extend that to number of casts per fight. float treeCasts = (float)Math.Floor(calcs.FightLength / 3) + 1.0f; // Partial cast: If the fight lasts 3.x minutes and x is less than 0.5 (30 sec tree duration), calculate a partial cast if ((int)calcs.FightLength % 3 == 0 && calcs.FightLength - (int)calcs.FightLength < 0.5) { treeCasts += (calcs.FightLength - (int)calcs.FightLength) / 0.5f - 1.0f; } treeDamage *= treeCasts; // Multiply by raid-wide damage increases. treeDamage *= (1 + calcs.BasicStats.BonusDamageMultiplier) * (1 + calcs.BasicStats.BonusPhysicalDamageMultiplier); // Calculate the DPS averaged over the fight length. float treeDPS = treeDamage / (calcs.FightLength * 60.0f); // Calculate mana usage for trees. float treeManaUsage = (float)Math.Ceiling(treeCasts) * CalculationsMoonkin.BaseMana * 0.12f; manaPool -= talents.ForceOfNature == 1 ? treeManaUsage : 0.0f; // Do Starfall calculations. bool starfallGlyph = talents.GlyphOfStarfall; Buff tier102PieceBuff = character.ActiveBuffs.Find(theBuff => theBuff.Name == "Lasherweave Regalia (T10) 2 Piece Bonus"); float numberOfStarHits = 10.0f; float starfallDamage = (talents.Starfall == 1) ? DoStarfallCalcs(baseSpellPower, baseHit, baseCrit, (1 + calcs.BasicStats.BonusDamageMultiplier) * (1 + calcs.BasicStats.BonusSpellPowerMultiplier) * (1 + calcs.BasicStats.BonusArcaneDamageMultiplier) * (1 + (talents.GlyphOfFocus ? 0.1f : 0.0f)), Wrath.CriticalDamageModifier, out numberOfStarHits) : 0.0f; float starfallCD = 1.5f - (starfallGlyph ? 0.5f : 0.0f); float numStarfallCasts = (float)Math.Floor(calcs.FightLength / starfallCD) + 1.0f; // Partial cast: If the difference between fight length and total starfall CD time is less than 10 seconds (duration), // calculate a partial cast float starfallDiff = calcs.FightLength * 60.0f - (numStarfallCasts - 1) * starfallCD * 60.0f; if (starfallDiff > 0 && starfallDiff < 10) { numStarfallCasts += starfallDiff / 60.0f / (1.0f / 6.0f) - 1.0f; } starfallDamage *= numStarfallCasts; float starfallManaUsage = (float)Math.Ceiling(numStarfallCasts) * CalculationsMoonkin.BaseMana * 0.39f; manaPool -= talents.Starfall == 1 ? starfallManaUsage : 0.0f; // Simple faerie fire mana calc float faerieFireCasts = (float)Math.Floor(calcs.FightLength / 5) + (calcs.FightLength % 5 != 0 ? 1.0f : 0.0f); float faerieFireMana = faerieFireCasts * CalculationsMoonkin.BaseMana * 0.08f; if (talents.ImprovedFaerieFire > 0) { manaPool -= faerieFireMana; } // Calculate effect of casting Starfall/Treants/ImpFF (regular FF is assumed to be provided by a feral) float globalCooldown = 1.5f / (1 + baseHaste) + calcs.Latency; float treantTime = (talents.ForceOfNature == 1) ? globalCooldown * (float)Math.Ceiling(treeCasts) : 0.0f; float starfallTime = (talents.Starfall == 1) ? globalCooldown * (float)Math.Ceiling(numStarfallCasts) : 0.0f; float faerieFireTime = (talents.ImprovedFaerieFire > 0) ? globalCooldown * faerieFireCasts : 0.0f; float totalTimeInRotation = calcs.FightLength * 60.0f - (treantTime + starfallTime + faerieFireTime); float percentTimeInRotation = totalTimeInRotation / (calcs.FightLength * 60.0f); #if RAWR3 BossOptions bossOpts = character.BossOptions; if (bossOpts == null) { bossOpts = new BossOptions(); } float fearShare = (bossOpts.FearingTargsDur / 1000) / bossOpts.FearingTargsFreq; float stunShare = (bossOpts.StunningTargsDur / 1000) / bossOpts.StunningTargsFreq; float invulnerableShare = bossOpts.TimeBossIsInvuln / bossOpts.BerserkTimer; List <Attack> attacks = bossOpts.GetFilteredAttackList(ATTACK_TYPES.AT_AOE); attacks.AddRange(bossOpts.GetFilteredAttackList(ATTACK_TYPES.AT_RANGED)); int movementCount = attacks.Count; float assumedMovementDuration = 2.0f; // Assume 2 seconds per move float accumulatedDurations = 0.0f; foreach (Attack a in attacks) { accumulatedDurations += a.AttackSpeed; } float movementShare = (movementCount == 0 ? 0 : assumedMovementDuration / (accumulatedDurations / movementCount) / (1 + calcs.BasicStats.MovementSpeed)); percentTimeInRotation -= movementShare + fearShare + stunShare + invulnerableShare; #endif float manaGained = manaPool - calcs.BasicStats.Mana; float oldArcaneMultiplier = calcs.BasicStats.BonusArcaneDamageMultiplier; float oldNatureMultiplier = calcs.BasicStats.BonusNatureDamageMultiplier; foreach (SpellRotation rot in rotations) { rot.Solver = this; float accumulatedDamage = 0.0f; float totalUpTime = 0.0f; float[] spellDetails = new float[NUM_SPELL_DETAILS]; List <ProcEffect> activatedEffects = new List <ProcEffect>(); List <ProcEffect> alwaysUpEffects = new List <ProcEffect>(); // Pre-calculate rotational variables with base stats rot.DamageDone(talents, calcs, baseSpellPower, baseHit, baseCrit, baseHaste); // Reset variables modified in the pre-loop to base values float currentSpellPower = baseSpellPower; float currentCrit = baseCrit; float currentHaste = baseHaste; calcs.BasicStats.BonusArcaneDamageMultiplier = oldArcaneMultiplier; calcs.BasicStats.BonusNatureDamageMultiplier = oldNatureMultiplier; // Calculate spell power/spell damage modifying trinkets in a separate pre-loop foreach (ProcEffect proc in procEffects) { if (proc.Effect.Stats.SpellPower > 0 || proc.Effect.Stats.Spirit > 0) { float procSpellPower = proc.Effect.Stats.SpellPower; if (proc.Effect.Stats.Spirit > 0) { procSpellPower += proc.Effect.Stats.Spirit * (0.1f * talents.ImprovedMoonkinForm); } float triggerInterval = 0.0f, triggerChance = 1.0f; switch (proc.Effect.Trigger) { case Trigger.DamageDone: case Trigger.DamageOrHealingDone: triggerInterval = ((rot.Duration / rot.CastCount) + (rot.Duration / (rot.MoonfireTicks + rot.InsectSwarmTicks))) / 2.0f; break; case Trigger.Use: break; case Trigger.SpellHit: case Trigger.DamageSpellHit: triggerInterval = rot.Duration / rot.CastCount; triggerChance = GetSpellHit(calcs); break; case Trigger.SpellCrit: case Trigger.DamageSpellCrit: triggerInterval = rot.Duration / (rot.CastCount - rot.InsectSwarmCasts); triggerChance = baseCrit; break; case Trigger.SpellCast: case Trigger.DamageSpellCast: triggerInterval = rot.Duration / rot.CastCount; break; case Trigger.MoonfireCast: triggerInterval = rot.Duration / rot.MoonfireCasts; break; case Trigger.DoTTick: case Trigger.InsectSwarmOrMoonfireTick: triggerInterval = rot.Duration / (rot.InsectSwarmTicks + rot.MoonfireTicks); break; case Trigger.MoonfireTick: triggerInterval = rot.Duration / rot.MoonfireTicks; break; case Trigger.InsectSwarmTick: triggerInterval = rot.Duration / rot.InsectSwarmTicks; break; default: triggerChance = 0.0f; break; } if (triggerChance > 0) { currentSpellPower += (proc.Effect.MaxStack > 1 ? proc.Effect.GetAverageStackSize(triggerInterval, triggerChance, 3.0f, calcs.FightLength * 60.0f) : 1) * proc.Effect.GetAverageUptime(triggerInterval, triggerChance) * procSpellPower; } } // 2T10 (both if statements, which is why it isn't else-if) if (proc.Effect.Stats.BonusArcaneDamageMultiplier > 0) { calcs.BasicStats.BonusArcaneDamageMultiplier += proc.Effect.GetAverageUptime(rot.Duration / rot.CastCount, 1f) * proc.Effect.Stats.BonusArcaneDamageMultiplier; } if (proc.Effect.Stats.BonusNatureDamageMultiplier > 0) { calcs.BasicStats.BonusNatureDamageMultiplier += proc.Effect.GetAverageUptime(rot.Duration / rot.CastCount, 1f) * proc.Effect.Stats.BonusNatureDamageMultiplier; } if (proc.Effect.Stats._rawSpecialEffectDataSize > 0) { SpecialEffect childEffect = proc.Effect.Stats._rawSpecialEffectData[0]; // Nevermelting Ice Crystal if (childEffect.Stats.CritRating != 0) { float maxStack = proc.Effect.Stats.CritRating; float numNegativeStacks = childEffect.GetAverageStackSize(rot.Duration / (rot.CastCount - rot.InsectSwarmCasts), baseCrit, 3.0f, proc.Effect.Duration); float averageNegativeValue = childEffect.Stats.CritRating * numNegativeStacks; float averageCritRating = maxStack + averageNegativeValue; currentCrit += StatConversion.GetSpellCritFromRating(averageCritRating * proc.Effect.GetAverageUptime(0f, 1f)); } // Fetish of Volatile Power else if (childEffect.Stats.HasteRating != 0) { currentHaste += StatConversion.GetSpellHasteFromRating(childEffect.Stats.HasteRating * childEffect.GetAverageStackSize(rot.Duration / rot.CastCount, 1f, 3.0f, proc.Effect.Duration) * proc.Effect.GetAverageUptime(0f, 1f)); } } } // Calculate damage and mana contributions for non-stat-boosting trinkets // Separate stat-boosting proc trinkets into their own list foreach (ProcEffect proc in procEffects) { if (proc.CalculateDPS != null) { accumulatedDamage += proc.CalculateDPS(rot, calcs, currentSpellPower, baseHit, currentCrit, currentHaste) * rot.Duration; } else if (proc.Activate != null) { float upTime = proc.UpTime(rot, calcs); // Procs with 100% uptime should be activated and not put into the combination loop if (upTime == 1) { alwaysUpEffects.Add(proc); proc.Activate(character, calcs, ref currentSpellPower, ref baseHit, ref currentCrit, ref currentHaste); } // Procs with uptime 0 < x < 100 should be activated else if (upTime > 0) { activatedEffects.Add(proc); } } else if (proc.CalculateMP5 != null) { manaGained += proc.CalculateMP5(rot, calcs, currentSpellPower, baseHit, currentCrit, currentHaste) / 5.0f * calcs.FightLength * 60.0f; } } // Calculate stat-boosting trinkets, taking into effect interactions with other stat-boosting procs int sign = 1; Dictionary <int, float> cachedDamages = new Dictionary <int, float>(); Dictionary <int, float> cachedUptimes = new Dictionary <int, float>(); Dictionary <int, float[]> cachedDetails = new Dictionary <int, float[]>(); // Iterate over the entire set of trinket combinations (each trinket by itself, 2 at a time, ...) for (int i = 1; i <= activatedEffects.Count; ++i) { // Create a new combination generator for this "level" of trinket interaction CombinationGenerator gen = new CombinationGenerator(activatedEffects.Count, i); // Iterate over all combinations while (gen.HasNext()) { float tempUpTime = 1.0f; int[] vals = gen.GetNext(); int pairs = 0; int lengthCounter = 0; // Activate the trinkets, calculate the damage and uptime, then deactivate them foreach (int idx in vals) { pairs |= 1 << idx; ++lengthCounter; activatedEffects[idx].Activate(character, calcs, ref currentSpellPower, ref baseHit, ref currentCrit, ref currentHaste); } float tempDPS = rot.DamageDone(talents, calcs, currentSpellPower, baseHit, currentCrit, currentHaste) / rot.Duration; spellDetails[0] = Starfire.DamagePerHit; spellDetails[1] = Wrath.DamagePerHit; spellDetails[2] = Moonfire.DamagePerHit + Moonfire.DotEffect.DamagePerHit; spellDetails[3] = InsectSwarm.DotEffect.DamagePerHit; spellDetails[4] = Starfire.CastTime; spellDetails[5] = Wrath.CastTime; spellDetails[6] = Moonfire.CastTime; spellDetails[7] = InsectSwarm.CastTime; spellDetails[8] = Starfire.NGCastTime; spellDetails[9] = Wrath.NGCastTime; spellDetails[10] = Starfire.ManaCost; spellDetails[11] = Wrath.ManaCost; spellDetails[12] = Moonfire.ManaCost; spellDetails[13] = InsectSwarm.ManaCost; foreach (int idx in vals) { tempUpTime *= activatedEffects[idx].UpTime(rot, calcs); activatedEffects[idx].Deactivate(character, calcs, ref currentSpellPower, ref baseHit, ref currentCrit, ref currentHaste); } if (tempUpTime == 0) { continue; } // Adjust previous probability tables by the current factor // At the end of the algorithm, this ensures that the probability table will contain the individual // probabilities of each effect or set of effects. // These adjustments only need to be made for higher levels of the table, and if the current probability is > 0. if (lengthCounter > 1) { List <int> keys = new List <int>(cachedUptimes.Keys); foreach (int subset in keys) { // Truly a subset? if ((pairs & subset) != subset) { continue; } // Calculate the "layer" of the current subset by getting the set bit count. int subsetLength = 0; for (int j = subset; j > 0; ++subsetLength) { j &= --j; } // Set the sign of the operation: Evenly separated layers are added, oddly separated layers are subtracted int newSign = ((lengthCounter - subsetLength) % 2 == 0) ? 1 : -1; // Adjust by current uptime * sign of operation. cachedUptimes[subset] += newSign * tempUpTime; } } // Cache the results to be calculated later cachedUptimes[pairs] = tempUpTime; cachedDamages[pairs] = tempDPS; cachedDetails[pairs] = new float[NUM_SPELL_DETAILS]; Array.Copy(spellDetails, cachedDetails[pairs], NUM_SPELL_DETAILS); totalUpTime += sign * tempUpTime; } sign = -sign; } float accumulatedDPS = 0.0f; Array.Clear(spellDetails, 0, spellDetails.Length); // Apply the above-calculated probabilities to the previously stored damage calculations and add to the result. foreach (KeyValuePair <int, float> kvp in cachedUptimes) { if (kvp.Value == 0) { continue; } accumulatedDPS += kvp.Value * cachedDamages[kvp.Key]; for (int i = 0; i < NUM_SPELL_DETAILS; ++i) { spellDetails[i] += kvp.Value * cachedDetails[kvp.Key][i]; } } float damageDone = rot.DamageDone(talents, calcs, currentSpellPower, baseHit, currentCrit, currentHaste); accumulatedDPS += (1 - totalUpTime) * damageDone / rot.Duration; spellDetails[0] += (1 - totalUpTime) * Starfire.DamagePerHit; spellDetails[1] += (1 - totalUpTime) * Wrath.DamagePerHit; spellDetails[2] += (1 - totalUpTime) * Moonfire.DamagePerHit + Moonfire.DotEffect.DamagePerHit; spellDetails[3] += (1 - totalUpTime) * InsectSwarm.DotEffect.DamagePerHit; spellDetails[4] += (1 - totalUpTime) * Starfire.CastTime; spellDetails[5] += (1 - totalUpTime) * Wrath.CastTime; spellDetails[6] += (1 - totalUpTime) * Moonfire.CastTime; spellDetails[7] += (1 - totalUpTime) * InsectSwarm.CastTime; spellDetails[8] += (1 - totalUpTime) * Starfire.NGCastTime; spellDetails[9] += (1 - totalUpTime) * Wrath.NGCastTime; spellDetails[10] += (1 - totalUpTime) * Starfire.ManaCost; spellDetails[11] += (1 - totalUpTime) * Wrath.ManaCost; spellDetails[12] += (1 - totalUpTime) * Moonfire.ManaCost; spellDetails[13] += (1 - totalUpTime) * InsectSwarm.ManaCost; accumulatedDamage += accumulatedDPS * rot.Duration; float burstDPS = accumulatedDamage / rot.Duration * percentTimeInRotation; float sustainedDPS = burstDPS; float timeToOOM = (manaPool / rot.RotationData.ManaUsed) * rot.Duration; if (timeToOOM <= 0) { timeToOOM = calcs.FightLength * 60.0f; // Happens when ManaUsed is less than 0 } if (timeToOOM < calcs.FightLength * 60.0f) { rot.RotationData.TimeToOOM = new TimeSpan(0, (int)(timeToOOM / 60), (int)(timeToOOM % 60)); sustainedDPS = burstDPS * timeToOOM / (calcs.FightLength * 60.0f); } float t10StarfallDamage = starfallDamage; // Approximate the effect of the 2T10 set bonus if (tier102PieceBuff != null && character.DruidTalents.OmenOfClarity == 1) { Stats.SpecialEffectEnumerator enumerator = tier102PieceBuff.Stats.SpecialEffects(); enumerator.MoveNext(); SpecialEffect effect = enumerator.Current; float upTime = effect.GetAverageUptime(rot.Duration / rot.CastCount, 1f); t10StarfallDamage = upTime * (starfallDamage * (1 + effect.Stats.BonusArcaneDamageMultiplier)) + (1 - upTime) * starfallDamage; } float starfallDPS = t10StarfallDamage / (calcs.FightLength * 60.0f); burstDPS += trinketDPS + treeDPS + starfallDPS; sustainedDPS += trinketDPS + treeDPS + starfallDPS; rot.StarfallDamage = t10StarfallDamage / numStarfallCasts; rot.StarfallStars = numberOfStarHits; rot.RotationData.BurstDPS = burstDPS; rot.RotationData.DPS = sustainedDPS; rot.StarfireAvgHit = spellDetails[0]; rot.WrathAvgHit = spellDetails[1]; rot.MoonfireAvgHit = spellDetails[2]; rot.InsectSwarmAvgHit = spellDetails[3]; rot.StarfireAvgCast = spellDetails[4]; rot.WrathAvgCast = spellDetails[5]; rot.MoonfireCastTime = spellDetails[6]; rot.InsectSwarmCastTime = spellDetails[7]; rot.StarfireNGCastTime = spellDetails[8]; rot.WrathNGCastTime = spellDetails[9]; rot.StarfireManaCost = spellDetails[10]; rot.WrathManaCost = spellDetails[11]; rot.MoonfireManaCost = spellDetails[12]; rot.InsectSwarmManaCost = spellDetails[13]; // Update the sustained DPS rotation if any one of the following three cases is true: // 1) No user rotation is selected and sustained DPS is maximum // 2) A user rotation is selected, Eclipse is not present, and the user rotation matches the current rotation // 3) A user rotation is selected, Eclipse is present, and the user rotation's dot spells matches this rotation's if ((calcOpts.UserRotation == "None" && sustainedDPS > maxDamageDone) || (character.DruidTalents.Eclipse == 0 && calcOpts.UserRotation == rot.Name) || (character.DruidTalents.Eclipse > 0 && (calcOpts.UserRotation == rot.Name.Replace("Filler", "SF") || calcOpts.UserRotation == rot.Name.Replace("Filler", "W")))) { maxDamageDone = sustainedDPS; maxRotation = rot; } if (burstDPS > maxBurstDamageDone) { maxBurstDamageDone = burstDPS; maxBurstRotation = rot; } rot.ManaGained += manaGained / (calcs.FightLength * 60.0f) * rot.Duration; rot.RotationData.ManaGained += manaGained / (calcs.FightLength * 60.0f) * rot.Duration; if (rot.Name.Contains("Filler")) { cachedResults[rot.Name.Replace("Filler", "SF")] = rot.RotationData; cachedResults[rot.Name.Replace("Filler", "W")] = rot.RotationData; } else { cachedResults[rot.Name] = rot.RotationData; } // Deactivate always-up procs foreach (ProcEffect proc in alwaysUpEffects) { proc.Deactivate(character, calcs, ref currentSpellPower, ref baseHit, ref currentCrit, ref currentHaste); } } // Present the findings to the user. calcs.TreantDamage = treeDamage / treeCasts; calcs.StarfallMana = starfallManaUsage / numStarfallCasts; calcs.SelectedRotation = maxRotation; calcs.BurstDPSRotation = maxBurstRotation; calcs.SubPoints = new float[] { maxDamageDone, maxBurstDamageDone }; calcs.OverallPoints = calcs.SubPoints[0] + calcs.SubPoints[1]; calcs.Rotations = cachedResults; }
private float GetAverageFactor(SpecialEffect effect) { float triggerInterval; float triggerChance; if (GetTriggerData(effect, out triggerInterval, out triggerChance)) { float durationMultiplier = 1; if (effect.LimitedToExecutePhase) { durationMultiplier = CastingState.CalculationOptions.MoltenFuryPercentage; } if (effect.MaxStack > 1) { return durationMultiplier * effect.GetAverageStackSize(triggerInterval, triggerChance, 3f, durationMultiplier * CastingState.CalculationOptions.FightDuration); } else if (effect.Duration > 0) { return durationMultiplier * effect.GetAverageUptime(triggerInterval, triggerChance, 3f, durationMultiplier * CastingState.CalculationOptions.FightDuration); } else { return durationMultiplier * effect.GetAverageProcsPerSecond(triggerInterval, triggerChance, 3f, durationMultiplier * CastingState.CalculationOptions.FightDuration); } } return 0; }