public SpecialDamageProcs(Character c, Stats charStats, int levelDelta, List <SpecialEffect> effectsList, Dictionary <Trigger, float> triggerIntervals, Dictionary <Trigger, float> triggerChances, float fightDuration, float armorDmgReduc) { Char = c; StatS = charStats; LevelDelta = levelDelta; EffectsList = effectsList; TriggerIntervals = triggerIntervals; TriggerChances = triggerChances; FightDuration = fightDuration; ArmorDmgReduc = armorDmgReduc; CreateDictionaries(); if (TriggerIntervals.ContainsKey(Trigger.OffHandHit) || TriggerIntervals.ContainsKey(Trigger.MainHandHit)) { if (c.MainHandEnchant != null) { Stats.SpecialEffectEnumerator e = c.MainHandEnchant.Stats.SpecialEffects(); while (e.MoveNext()) { MainHandEffects.Add(e.Current); } } if (c.MainHand != null && c.MainHand.Item != null) { Stats.SpecialEffectEnumerator e = c.MainHand.Item.Stats.SpecialEffects(); while (e.MoveNext()) { MainHandEffects.Add(e.Current); } } if (c.OffHandEnchant != null) { Stats.SpecialEffectEnumerator e = c.OffHandEnchant.Stats.SpecialEffects(); while (e.MoveNext()) { OffHandEffects.Add(e.Current); } } if (c.OffHand != null && c.OffHand.Item != null) { Stats.SpecialEffectEnumerator e = c.OffHand.Item.Stats.SpecialEffects(); while (e.MoveNext()) { OffHandEffects.Add(e.Current); } } } }
public StatsSpecialEffects(Character character, Stats stats, CalculationOptionsEnhance calcOpts) { _character = character; _stats = stats; _cs = new CombatStats(_character, _stats, calcOpts); if (character.MainHandEnchant != null) { Stats.SpecialEffectEnumerator mhEffects = character.MainHandEnchant.Stats.SpecialEffects(); if (mhEffects.MoveNext()) { mainHandEnchant = mhEffects.Current; } } if (_character.ShamanTalents.DualWield == 1 && character.OffHandEnchant != null) { Stats.SpecialEffectEnumerator ohEffects = character.OffHandEnchant.Stats.SpecialEffects(); if (ohEffects.MoveNext()) { offHandEnchant = ohEffects.Current; } } }
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; }