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; }
public void Solve(Character character, ref CharacterCalculationsMoonkin calcs) { CalculationOptionsMoonkin calcOpts = character.CalculationOptions as CalculationOptionsMoonkin; DruidTalents talents = character.DruidTalents; procEffects = new List<ProcEffect>(); UpdateSpells(character, ref calcs); float trinketDPS = 0.0f; float baseSpellPower = calcs.SpellPower; float baseHit = 1 - Math.Max(0, calcs.SpellHitCap - calcs.SpellHit); float baseCrit = calcs.SpellCrit; float baseHaste = calcs.SpellHaste; float baseMastery = calcs.Mastery; float sub35PercentTime = (float)(character.BossOptions.Under20Perc + character.BossOptions.Under35Perc); BuildProcList(calcs); float maxDamageDone = 0.0f, maxBurstDamageDone = 0.0f; SpellRotation maxBurstRotation = Rotations[0]; SpellRotation maxRotation = Rotations[0]; float manaPool = GetEffectiveManaPool(character, calcOpts, calcs); float manaGained = manaPool - calcs.BasicStats.Mana; float oldArcaneMultiplier = calcs.BasicStats.BonusArcaneDamageMultiplier; float oldNatureMultiplier = calcs.BasicStats.BonusNatureDamageMultiplier; int rotationIndex = 1; foreach (SpellRotation rot in Rotations) { if (rot.RotationData.Name == "None") continue; rot.Solver = this; // Reset variables modified in the pre-loop to base values float currentSpellPower = baseSpellPower; float currentCrit = baseCrit; float currentHaste = baseHaste; float currentMastery = baseMastery; float currentTrinketDPS = trinketDPS; calcs.BasicStats.BonusArcaneDamageMultiplier = oldArcaneMultiplier; calcs.BasicStats.BonusNatureDamageMultiplier = oldNatureMultiplier; 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>(); float baselineDPS = rot.DamageDone(character, calcs, calcOpts.TreantLifespan, currentSpellPower, baseHit, currentCrit, currentHaste, currentMastery, calcOpts.Latency); // Calculate spell power/spell damage modifying trinkets in a separate pre-loop // Add spell crit effects here as well, since they no longer affect timing // Add Intellect procs here, since they are a combination of spell power and spell crit foreach (ProcEffect proc in procEffects) { bool handled = false; if (proc.Effect.Stats.SpellPower > 0 || proc.Effect.Stats.CritRating > 0 || proc.Effect.Stats.MasteryRating > 0 || proc.Effect.Stats.Intellect > 0 || proc.Effect.Stats.HighestStat > 0) { handled = true; float procIntellect = (float)Math.Floor((1 + calcs.BasicStats.BonusIntellectMultiplier) * (proc.Effect.Stats.Intellect + proc.Effect.Stats.HighestStat)); float procSpellPower = (float)Math.Floor((1 + calcs.BasicStats.BonusSpellPowerMultiplier) * (proc.Effect.Stats.SpellPower + procIntellect)); float procSpellCrit = StatConversion.GetSpellCritFromRating(proc.Effect.Stats.CritRating) + StatConversion.GetSpellCritFromIntellect(procIntellect); float procMastery = StatConversion.GetMasteryFromRating(proc.Effect.Stats.MasteryRating); float triggerInterval = 0.0f, triggerChance = 1.0f; switch (proc.Effect.Trigger) { case Trigger.DamageDone: case Trigger.DamageOrHealingDone: triggerInterval = ((rot.RotationData.Duration / rot.RotationData.CastCount) + (rot.RotationData.Duration / (rot.RotationData.MoonfireTicks + rot.RotationData.InsectSwarmTicks))) / 2.0f; break; case Trigger.Use: break; case Trigger.SpellHit: case Trigger.DamageSpellHit: triggerInterval = rot.RotationData.Duration / rot.RotationData.CastCount; triggerChance = baseHit; break; case Trigger.SpellCrit: case Trigger.DamageSpellCrit: triggerInterval = rot.RotationData.Duration / (rot.RotationData.CastCount - rot.RotationData.InsectSwarmCasts); triggerChance = baseCrit; break; case Trigger.SpellCast: case Trigger.DamageSpellCast: triggerInterval = rot.RotationData.Duration / rot.RotationData.CastCount; break; case Trigger.MoonfireCast: triggerInterval = rot.RotationData.Duration / rot.RotationData.MoonfireCasts; break; case Trigger.DoTTick: triggerInterval = rot.RotationData.Duration / (rot.RotationData.InsectSwarmTicks + rot.RotationData.MoonfireTicks); break; case Trigger.MoonfireTick: triggerInterval = rot.RotationData.Duration / rot.RotationData.MoonfireTicks; break; case Trigger.InsectSwarmTick: triggerInterval = rot.RotationData.Duration / rot.RotationData.InsectSwarmTicks; break; default: triggerChance = 0.0f; break; } if (triggerChance > 0) { float durationMultiplier = proc.Effect.LimitedToExecutePhase ? sub35PercentTime : 1f; currentSpellPower += (proc.Effect.MaxStack > 1 ? proc.Effect.GetAverageStackSize(triggerInterval, triggerChance, 3.0f, character.BossOptions.BerserkTimer * 60.0f * durationMultiplier) : 1) * proc.Effect.GetAverageUptime(triggerInterval, triggerChance, 3.0f, character.BossOptions.BerserkTimer * 60.0f) * procSpellPower * durationMultiplier; currentCrit += (proc.Effect.MaxStack > 1 ? proc.Effect.GetAverageStackSize(triggerInterval, triggerChance, 3.0f, character.BossOptions.BerserkTimer * 60.0f * durationMultiplier) : 1) * proc.Effect.GetAverageUptime(triggerInterval, triggerChance, 3.0f, character.BossOptions.BerserkTimer * 60.0f) * procSpellCrit * durationMultiplier; currentMastery += (proc.Effect.MaxStack > 1 ? proc.Effect.GetAverageStackSize(triggerInterval, triggerChance, 3.0f, character.BossOptions.BerserkTimer * 60.0f * durationMultiplier) : 1) * proc.Effect.GetAverageUptime(triggerInterval, triggerChance, 3.0f, character.BossOptions.BerserkTimer * 60.0f) * procMastery * durationMultiplier; } } // 2T10 (both if statements, which is why it isn't else-if) if (proc.Effect.Stats.BonusArcaneDamageMultiplier > 0) { handled = true; calcs.BasicStats.BonusArcaneDamageMultiplier += proc.Effect.GetAverageUptime(rot.RotationData.Duration / rot.RotationData.CastCount, 1f, 3.0f, character.BossOptions.BerserkTimer * 60.0f) * proc.Effect.Stats.BonusArcaneDamageMultiplier; } if (proc.Effect.Stats.BonusNatureDamageMultiplier > 0) { handled = true; calcs.BasicStats.BonusNatureDamageMultiplier += proc.Effect.GetAverageUptime(rot.RotationData.Duration / rot.RotationData.CastCount, 1f, 3.0f, character.BossOptions.BerserkTimer * 60.0f) * proc.Effect.Stats.BonusNatureDamageMultiplier; } // Variable Pulse Lightning Capacitor // This might catch some other effects, I probably need a better way to differentiate if (proc.Effect.Trigger == Trigger.DamageSpellCrit && proc.Effect.Stats.NatureDamage > 0) { float procInterval = rot.RotationData.Duration / (rot.RotationData.CastCount - rot.RotationData.InsectSwarmCasts + rot.RotationData.DotTicks); currentTrinketDPS += proc.Effect.GetAverageProcsPerSecond(procInterval, currentCrit, 3.0f, character.BossOptions.BerserkTimer * 60.0f) * proc.Effect.Stats.NatureDamage; } // Nested special effects if (proc.Effect.Stats._rawSpecialEffectDataSize > 0) { handled = true; SpecialEffect childEffect = proc.Effect.Stats._rawSpecialEffectData[0]; // Heart of Ignacious if (childEffect.Stats.SpellPower > 0) { float averageStack = childEffect.GetAverageStackSize(rot.RotationData.Duration / rot.RotationData.CastCount, baseHit, 3.0f, proc.Effect.Duration); currentSpellPower += (float)Math.Floor((1 + calcs.BasicStats.BonusSpellPowerMultiplier) * childEffect.Stats.SpellPower) * averageStack * proc.Effect.GetAverageUptime(rot.RotationData.Duration / rot.RotationData.CastCount, baseHit); } // 4T11 if (childEffect.Stats.SpellCrit != 0) { float maxStack = proc.Effect.Stats.SpellCrit; float numNegativeStacks = childEffect.GetAverageStackSize(rot.RotationData.Duration / (rot.RotationData.CastCount - rot.RotationData.InsectSwarmCasts), Math.Min(1.0f, baseCrit + maxStack), 3.0f, proc.Effect.Duration); float averageNegativeValue = childEffect.Stats.SpellCrit * numNegativeStacks; float averageCrit = maxStack + averageNegativeValue; currentCrit += averageCrit * proc.Effect.GetAverageUptime(rot.RotationData.Duration / 2f, 1f, 3.0f, character.BossOptions.BerserkTimer * 60.0f); } } if (!handled) { if (proc.CalculateDPS != null) { accumulatedDamage += proc.CalculateDPS(rot, calcs, character.BossOptions.BerserkTimer, currentSpellPower, baseHit, currentCrit, currentHaste) * rot.RotationData.Duration; } if (proc.Activate != null) { float upTime = proc.UpTime(rot, calcs, character.BossOptions.BerserkTimer, (float)(character.BossOptions.Under35Perc + character.BossOptions.Under20Perc)); // 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, ref currentMastery); } // Procs with uptime 0 < x < 100 should be activated else if (upTime > 0) activatedEffects.Add(proc); } if (proc.CalculateMP5 != null) { manaGained += proc.CalculateMP5(rot, calcs, character.BossOptions.BerserkTimer, currentSpellPower, baseHit, currentCrit, currentHaste) / 5.0f * character.BossOptions.BerserkTimer * 60.0f; } } } // Calculate stat-boosting trinkets, taking into effect interactions with other stat-boosting procs int sign = 1; float[] cachedDamages = new float[1 << activatedEffects.Count]; float[] cachedUptimes = new float[1 << activatedEffects.Count]; float[,] cachedDetails = new float[1 << activatedEffects.Count, NUM_SPELL_DETAILS]; List<int> calculatedPairs = new List<int>(); // 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, ref currentMastery); } currentCrit = (float)Math.Min(1.0f, currentCrit); float tempDPS = rot.DamageDone(character, calcs, calcOpts.TreantLifespan, currentSpellPower, baseHit, currentCrit, currentHaste, currentMastery, calcOpts.Latency) / rot.RotationData.Duration; spellDetails[0] = rot.RotationData.StarfireAvgHit; spellDetails[1] = rot.RotationData.WrathAvgHit; spellDetails[2] = rot.RotationData.MoonfireAvgHit; spellDetails[3] = rot.RotationData.InsectSwarmAvgHit; spellDetails[4] = rot.RotationData.StarSurgeAvgHit; spellDetails[5] = rot.RotationData.StarfireAvgCast; spellDetails[6] = rot.RotationData.WrathAvgCast; spellDetails[7] = rot.RotationData.MoonfireAvgCast; spellDetails[8] = rot.RotationData.InsectSwarmAvgCast; spellDetails[9] = rot.RotationData.StarSurgeAvgCast; spellDetails[10] = rot.RotationData.AverageInstantCast; spellDetails[11] = rot.RotationData.StarfireAvgEnergy; spellDetails[12] = rot.RotationData.WrathAvgEnergy; spellDetails[13] = rot.RotationData.StarSurgeAvgEnergy; spellDetails[14] = rot.RotationData.TreantDamage; spellDetails[15] = rot.RotationData.StarfallDamage; spellDetails[16] = rot.RotationData.MushroomDamage; foreach (int idx in vals) { tempUpTime *= activatedEffects[idx].UpTime(rot, calcs, character.BossOptions.BerserkTimer, (float)(character.BossOptions.Under35Perc + character.BossOptions.Under20Perc)); activatedEffects[idx].Deactivate(character, calcs, ref currentSpellPower, ref baseHit, ref currentCrit, ref currentHaste, ref currentMastery); } 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) { foreach (int subset in calculatedPairs) { // 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; for (int idx = 0; idx < NUM_SPELL_DETAILS; ++idx) { cachedDetails[pairs, idx] = spellDetails[idx]; } calculatedPairs.Add(pairs); 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. for (int idx = 0; idx < cachedUptimes.Length; ++idx) { if (cachedUptimes[idx] == 0) continue; accumulatedDPS += cachedUptimes[idx] * cachedDamages[idx]; for (int i = 0; i < NUM_SPELL_DETAILS; ++i) { spellDetails[i] += cachedUptimes[idx] * cachedDetails[idx,i]; } } float damageDone = rot.DamageDone(character, calcs, calcOpts.TreantLifespan, currentSpellPower, baseHit, currentCrit, currentHaste, currentMastery, calcOpts.Latency); accumulatedDPS += (1 - totalUpTime) * damageDone / rot.RotationData.Duration; spellDetails[0] += (1 - totalUpTime) * rot.RotationData.StarfireAvgHit; spellDetails[1] += (1 - totalUpTime) * rot.RotationData.WrathAvgHit; spellDetails[2] += (1 - totalUpTime) * rot.RotationData.MoonfireAvgHit; spellDetails[3] += (1 - totalUpTime) * rot.RotationData.InsectSwarmAvgHit; spellDetails[4] += (1 - totalUpTime) * rot.RotationData.StarSurgeAvgHit; spellDetails[5] += (1 - totalUpTime) * rot.RotationData.StarfireAvgCast; spellDetails[6] += (1 - totalUpTime) * rot.RotationData.WrathAvgCast; spellDetails[7] += (1 - totalUpTime) * rot.RotationData.MoonfireAvgCast; spellDetails[8] += (1 - totalUpTime) * rot.RotationData.InsectSwarmAvgCast; spellDetails[9] += (1 - totalUpTime) * rot.RotationData.StarSurgeAvgCast; spellDetails[10] += (1 - totalUpTime) * rot.RotationData.AverageInstantCast; spellDetails[11] += (1 - totalUpTime) * rot.RotationData.StarfireAvgEnergy; spellDetails[12] += (1 - totalUpTime) * rot.RotationData.WrathAvgEnergy; spellDetails[13] += (1 - totalUpTime) * rot.RotationData.StarSurgeAvgEnergy; spellDetails[14] += (1 - totalUpTime) * rot.RotationData.TreantDamage; spellDetails[15] += (1 - totalUpTime) * rot.RotationData.StarfallDamage; spellDetails[16] += (1 - totalUpTime) * rot.RotationData.MushroomDamage; float burstDPS = accumulatedDPS + accumulatedDamage / rot.RotationData.Duration; float sustainedDPS = burstDPS; // Mana calcs: // Main rotation - all spells // Movement rotation - Lunar Shower MF, IS, Shooting Stars procs, and Starfall only rot.RotationData.ManaGained += manaGained / (character.BossOptions.BerserkTimer * 60.0f) * rot.RotationData.Duration; float timeToOOM = manaPool / ((rot.RotationData.ManaUsed - rot.RotationData.ManaGained) / rot.RotationData.Duration); if (timeToOOM <= 0) timeToOOM = character.BossOptions.BerserkTimer * 60.0f; // Happens when ManaUsed is less than 0 if (timeToOOM < character.BossOptions.BerserkTimer * 60.0f) { rot.RotationData.TimeToOOM = new TimeSpan(0, (int)(timeToOOM / 60), (int)(timeToOOM % 60)); sustainedDPS = burstDPS * timeToOOM / (character.BossOptions.BerserkTimer * 60.0f); } burstDPS += currentTrinketDPS; sustainedDPS += currentTrinketDPS; rot.RotationData.SustainedDPS = sustainedDPS; rot.RotationData.BurstDPS = burstDPS; rot.RotationData.StarfireAvgHit = spellDetails[0]; rot.RotationData.WrathAvgHit = spellDetails[1]; rot.RotationData.MoonfireAvgHit = spellDetails[2]; rot.RotationData.InsectSwarmAvgHit = spellDetails[3]; rot.RotationData.StarSurgeAvgHit = spellDetails[4]; rot.RotationData.StarfireAvgCast = spellDetails[5]; rot.RotationData.WrathAvgCast = spellDetails[6]; rot.RotationData.MoonfireAvgCast = spellDetails[7]; rot.RotationData.InsectSwarmAvgCast = spellDetails[8]; rot.RotationData.StarSurgeAvgCast = spellDetails[9]; rot.RotationData.AverageInstantCast = spellDetails[10]; rot.RotationData.StarfireAvgEnergy = spellDetails[11]; rot.RotationData.WrathAvgEnergy = spellDetails[12]; rot.RotationData.StarSurgeAvgEnergy = spellDetails[13]; rot.RotationData.TreantDamage = spellDetails[14]; rot.RotationData.StarfallDamage = spellDetails[15]; rot.RotationData.MushroomDamage = spellDetails[16]; // 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) || rot.RotationData.Name == calcOpts.UserRotation) { maxDamageDone = sustainedDPS; maxRotation = rot; } if (burstDPS > maxBurstDamageDone) { maxBurstDamageDone = burstDPS; maxBurstRotation = rot; } cachedResults[rotationIndex - 1] = rot.RotationData; // Deactivate always-up procs foreach (ProcEffect proc in alwaysUpEffects) { proc.Deactivate(character, calcs, ref currentSpellPower, ref baseHit, ref currentCrit, ref currentHaste, ref currentMastery); } ++rotationIndex; } // Present the findings to the user. calcs.SelectedRotation = maxRotation.RotationData; calcs.BurstRotation = maxBurstRotation.RotationData; calcs.SubPoints = new float[] { maxBurstDamageDone, maxDamageDone }; calcs.OverallPoints = calcs.SubPoints[0] + calcs.SubPoints[1]; calcs.Rotations = cachedResults; }