public ComputedSpell(SpellData data, TreeStats stats) { this.Data = data; this.Stats = stats; this.DirectMultiplier = stats.DirectHealMultiplier; this.TickMultiplier = stats.PeriodicHealMultiplier; }
void addPassiveHealing(ActionDistribution dist, TreeStats stats) { dist.AddPassive((int)TreePassive.HealingTrinkets, stats.Healed * stats.DirectHealMultiplier * getCritMultiplier(stats, 0, 0)); if (InsectSwarm) dist.AddActionOnCooldown((int)TreeAction.InsectSwarm); }
void addSpecialDirectHeals(ActionDistribution dist, ComputedSpell[] spells, TreeStats stats, bool onTank, bool useCCs, double minRate, bool htFiller) { // TODO: possibly make configurable, it's disabled because in practice it's unlikely to have a clearcast exactly when Nature's Grace is to be triggered bool procNaturesGraceWithCCs = false; bool naturesGraceHandled = false; if (InsectSwarm) naturesGraceHandled = true; double dhrate = 0; double ccrate = spells[(int)TreeSpell.Lifebloom].TPS * 0.02f * Talents.MalfurionsGift; if (ccrate != 0.0f) { if (procNaturesGraceWithCCs && Talents.NaturesGrace > 0) { naturesGraceHandled = true; dist.AddAction(onTank ? (int)TreeAction.TankClearRegrowth : (int)TreeAction.RaidClearRegrowth, 1.0f / 60.0f * spells[(int)TreeSpell.Regrowth].Action.Time); ccrate -= 1.0f / 60.0f; dhrate += 1.0f / 60.0f; } if (useCCs) { dist.AddAction(onTank ? (int)TreeAction.TankClearHT : (int)TreeAction.RaidClearHT, ccrate * spells[(int)TreeSpell.HealingTouch].Action.Time); dhrate += ccrate; } } if (!naturesGraceHandled && Talents.NaturesGrace > 0) { dist.AddAction(onTank ? (int)TreeAction.TankRegrowth : (int)TreeAction.RaidRegrowth, 1.0f / 60.0f * spells[(int)TreeSpell.Regrowth].Action.Time); dhrate += 1.0f / 60.0f; } if (dhrate < minRate) { if (!htFiller) dist.AddAction(onTank ? (int)TreeAction.TankNourish : (int)TreeAction.RaidNourish, (minRate - dhrate) * spells[(int)TreeSpell.Nourish].Action.Time); else dist.AddAction(onTank ? (int)TreeAction.TankHealingTouch : (int)TreeAction.RaidHealingTouch, (minRate - dhrate) * spells[(int)TreeSpell.HealingTouch].Action.Time); } }
ContinuousAction[] computeDivisionActions(TreeStats stats, TreeComputedData data, ComputedSpell[] spells) { double healingTouchNSReduction = 0; bool swiftmendEatsRejuvenation = true; double swiftmendExtraTargets = 0; double lifebloomManaPerTick = 0; if (T12Count >= 2) lifebloomManaPerTick += CalculationsTree.BaseMana * 0.01 * 0.4; if (T12Count >= 4) swiftmendExtraTargets += opts.SwiftmendExtraHealEH; if (Talents.GlyphOfSwiftmend) swiftmendEatsRejuvenation = false; if (Talents.GlyphOfHealingTouch) healingTouchNSReduction = 10; data.LifebloomMPSGain = lifebloomManaPerTick * spells[(int)TreeSpell.Lifebloom].TPS; DiscreteAction[] actions = new DiscreteAction[(int)TreeAction.Count]; for (int i = 0; i < spells.Length; ++i) { actions[i] = spells[i].RaidAction; actions[spells.Length + i] = spells[i].TankAction; } #region Effective healing actions[(int)TreeAction.RaidTolLb].Direct *= opts.ToLLifebloomEH; actions[(int)TreeAction.RaidTolLb].Periodic *= opts.ToLLifebloomEH; actions[(int)TreeAction.RaidRejuvenation].Periodic *= opts.RejuvenationEH; actions[(int)TreeAction.RaidHealingTouch].Direct *= opts.HealingTouchEH; actions[(int)TreeAction.RaidNourish].Direct *= opts.NourishEH; actions[(int)TreeAction.RaidRegrowth].Direct *= opts.NourishEH; actions[(int)TreeAction.RaidRegrowth].Periodic *= opts.NourishEH; actions[(int)TreeAction.RaidWildGrowth].Periodic *= opts.WildGrowthEH; actions[(int)TreeAction.RaidSwiftmend].Periodic *= opts.SwiftmendEH; actions[(int)TreeAction.RaidTranquility].Direct *= opts.TranquilityEH; actions[(int)TreeAction.RaidTranquility].Periodic *= opts.TranquilityEH; #endregion #region Targets double defaultCritMultiplier = getCritMultiplier(stats, 0, 0); double wgTargets = (Talents.GlyphOfWildGrowth ? 6 : 5) + stats.TreeOfLifeUptime * 2; actions[(int)TreeAction.TankWildGrowth].Periodic += (wgTargets - 1) * actions[(int)TreeAction.RaidWildGrowth].Periodic * opts.TankRaidHealingWeight; actions[(int)TreeAction.RaidWildGrowth].Periodic *= wgTargets; actions[(int)TreeAction.TankWildGrowth].Ticks *= wgTargets; actions[(int)TreeAction.RaidWildGrowth].Ticks *= wgTargets; actions[(int)TreeAction.TankSwiftmend].Direct *= (1 + swiftmendExtraTargets * defaultCritMultiplier * stats.DirectHealMultiplier * opts.TankRaidHealingWeight); actions[(int)TreeAction.RaidSwiftmend].Direct *= (1 + swiftmendExtraTargets * defaultCritMultiplier * stats.DirectHealMultiplier); actions[(int)TreeAction.TankSwiftmend].Casts *= (1 + swiftmendExtraTargets); actions[(int)TreeAction.RaidSwiftmend].Casts *= (1 + swiftmendExtraTargets); actions[(int)TreeAction.TankSwiftmend].Periodic *= 1 + opts.TankRaidHealingWeight * Math.Max(3 * opts.EfflorescenceEH - 1.0, 0); actions[(int)TreeAction.RaidSwiftmend].Periodic *= 3 * opts.EfflorescenceEH; actions[(int)TreeAction.TankSwiftmend].Ticks *= Math.Max(3 * opts.EfflorescenceEH, 1); actions[(int)TreeAction.RaidSwiftmend].Ticks *= 3 * opts.EfflorescenceEH; #endregion #region Swiftmend if (swiftmendEatsRejuvenation) actions[(int)TreeAction.RaidSwiftmend].Direct -= actions[(int)TreeAction.RaidRejuvenation].Periodic * 0.5f; #endregion #region Lifebloom if (stats.TreeOfLifeUptime > 0) { // TODO: figure out how 2T12 works in ToL and adjust the code as needed //actions[(int)TreeAction.RaidTolLb].Mana -= lifebloomManaPerTick * spells[(int)TreeSpell.Lifebloom].Ticks; //actions[(int)TreeAction.TankTolLb].Mana -= lifebloomManaPerTick * spells[(int)TreeSpell.Lifebloom].Ticks; } else { actions[(int)TreeAction.RaidTolLb].Direct = 0; actions[(int)TreeAction.RaidTolLb].Periodic = 0; actions[(int)TreeAction.TankTolLb].Direct = 0; actions[(int)TreeAction.TankTolLb].Periodic = 0; } #endregion #region Rejuvenation actions[(int)TreeAction.TankRejuvenation].Cooldown = spells[(int)TreeSpell.Rejuvenation].Duration; #endregion #region Nature's Swiftness if (Talents.NaturesSwiftness > 0) { // Nature's Swiftness is actually additive double nshtMultiplier = 1 + 0.5 / (1 + stats.PassiveDirectHealBonus + spells[(int)TreeSpell.HealingTouch].ExtraDirectBonus); actions[(int)TreeAction.RaidSwiftHT] = actions[(int)TreeAction.RaidHealingTouch]; actions[(int)TreeAction.TankSwiftHT] = actions[(int)TreeAction.TankHealingTouch]; actions[(int)TreeAction.RaidSwiftHT].Time = stats.Haste.HastedGCD; actions[(int)TreeAction.TankSwiftHT].Time = stats.Haste.HastedGCD; actions[(int)TreeAction.RaidSwiftHT].Cooldown = 180 + opts.NaturesSwiftnessCastDelay; actions[(int)TreeAction.TankSwiftHT].Cooldown = 180 + opts.NaturesSwiftnessCastDelay; actions[(int)TreeAction.RaidSwiftHT].Direct *= nshtMultiplier; actions[(int)TreeAction.TankSwiftHT].Direct *= nshtMultiplier; if (healingTouchNSReduction > 0) { // add the NS effect as an amortized extra heal to each HT double swiftHtFraction = (healingTouchNSReduction / (180.0 + opts.NaturesSwiftnessCastDelay)); double htAddTime = swiftHtFraction * stats.Haste.HastedGCD; double htMulDirect = 1 + swiftHtFraction * nshtMultiplier; double htMulMana = 1 + swiftHtFraction; actions[(int)TreeAction.TankHealingTouch].Time += htAddTime; actions[(int)TreeAction.TankHealingTouch].Mana *= htMulMana; actions[(int)TreeAction.TankHealingTouch].Direct *= htMulDirect; actions[(int)TreeAction.RaidHealingTouch].Time += htAddTime; actions[(int)TreeAction.RaidHealingTouch].Mana *= htMulMana; actions[(int)TreeAction.RaidHealingTouch].Direct *= htMulDirect; } } #endregion #region Clearcasting actions[(int)TreeAction.RaidClearHT] = applyCC(actions[(int)TreeAction.RaidHealingTouch]); actions[(int)TreeAction.TankClearHT] = applyCC(actions[(int)TreeAction.TankHealingTouch]); actions[(int)TreeAction.RaidClearRegrowth] = applyCC(actions[(int)TreeAction.RaidRegrowth]); actions[(int)TreeAction.TankClearRegrowth] = applyCC(actions[(int)TreeAction.TankRegrowth]); if (stats.TreeOfLifeUptime > 0) { actions[(int)TreeAction.RaidTolLbCcHt] = buildTolLbCcHt(stats, spells[(int)TreeSpell.Lifebloom].Ticks, actions[(int)TreeAction.RaidTolLb], 1, actions[(int)TreeAction.RaidClearHT]); actions[(int)TreeAction.TankTolLbCcHt] = buildTolLbCcHt(stats, spells[(int)TreeSpell.Lifebloom].Ticks, actions[(int)TreeAction.RaidTolLb], opts.TankRaidHealingWeight, actions[(int)TreeAction.TankClearHT]); } #endregion #region Nature's Bounty actions[(int)TreeAction.RaidRj2NourishNB] = buildNourishNB(stats, actions[(int)TreeAction.RaidRejuvenation], spells[(int)TreeSpell.Rejuvenation].Duration, 2, 1, actions[(int)TreeAction.RaidNourish]); actions[(int)TreeAction.RaidRj3NourishNB] = buildNourishNB(stats, actions[(int)TreeAction.RaidRejuvenation], spells[(int)TreeSpell.Rejuvenation].Duration, 3, 1, actions[(int)TreeAction.RaidNourish]); actions[(int)TreeAction.TankRj2NourishNB] = buildNourishNB(stats, actions[(int)TreeAction.RaidRejuvenation], spells[(int)TreeSpell.Rejuvenation].Duration, 2, opts.TankRaidHealingWeight, actions[(int)TreeAction.TankNourish]); #endregion #region Additional actions data.LifebloomRefreshInterval = spells[(int)TreeSpell.Lifebloom].Duration - opts.LifebloomWastedDuration * stats.Haste.HastedSecond; if (data.LifebloomRefreshInterval < stats.Haste.HastedSecond * 3) data.LifebloomRefreshInterval = stats.Haste.HastedSecond * 3; actions[(int)TreeAction.ReLifebloom].Time = spells[(int)TreeSpell.Lifebloom].Action.Time; actions[(int)TreeAction.ReLifebloom].Mana = spells[(int)TreeSpell.Lifebloom].Action.Mana; actions[(int)TreeAction.ReLifebloom].Direct = 0; actions[(int)TreeAction.ReLifebloom].Periodic = 0; actions[(int)TreeAction.ReLifebloom].Cooldown = data.LifebloomRefreshInterval; // TODO: actually compute hit instead of assuming we have 0 hit; also maybe have an option/BossHandler value for the target level (there might a permanent add with lower level than the boss) actions[(int)TreeAction.InsectSwarm].Time = stats.Haste.HastedGCD / (1 - StatConversion.GetSpellMiss(character.Level - character.BossOptions.Level, false)); actions[(int)TreeAction.InsectSwarm].Mana = ((int)Math.Floor(CalculationsTree.BaseMana * 8 / 100f) - stats.SpellsManaCostReduction) * stats.SpellsManaCostMultiplier; actions[(int)TreeAction.InsectSwarm].Cooldown = 12 + Talents.Genesis * 2; #endregion ContinuousAction[] factions = new ContinuousAction[(int)TreeAction.Count]; for (int i = 0; i < (int)TreeAction.Count; ++i) factions[i] = new ContinuousAction(actions[i]); return factions; }
void computeDivisionSpellActions(TreeStats stats, TreeComputedData data, ComputedSpell[] spells) { for (int i = 0; i < (int)TreeSpell.Count; ++i) { spells[i].RaidAction = spells[i].Action; spells[i].TankAction = spells[i].Action; } double lifebloomExtraDirectCrit = 0; double lifebloomExtraTickCrit = 0; double regrowthExtraCrit = 0; double tankLivingSeed, raidLivingSeed; #region Talents if (T11Count >= 2) lifebloomExtraTickCrit += 0.05f; tankLivingSeed = raidLivingSeed = 0.1f * Talents.LivingSeed; raidLivingSeed *= opts.LivingSeedEH; regrowthExtraCrit += 0.2f * Talents.NaturesBounty; if (Talents.GlyphOfLifebloom) { lifebloomExtraDirectCrit += 0.1f; lifebloomExtraTickCrit += 0.1f; } #endregion #region Crit double defaultCritMultiplier = getCritMultiplier(stats, 0, 0); double raidLivingSeedCritMultiplier = getCritMultiplier(stats, 0, raidLivingSeed); double tankLivingSeedCritMultiplier = getCritMultiplier(stats, 0, tankLivingSeed); spells[(int)TreeSpell.Lifebloom].MultiplyDirect(getCritMultiplier(stats, lifebloomExtraDirectCrit, 0)); spells[(int)TreeSpell.Lifebloom].MultiplyPeriodic(getCritMultiplier(stats, lifebloomExtraTickCrit, 0)); spells[(int)TreeSpell.WildGrowth].MultiplyPeriodic(defaultCritMultiplier); spells[(int)TreeSpell.Tranquility].Multiply(defaultCritMultiplier); // gotem doesn't crit, but is affected by Symbiosis, according to TreeCalcs spells[(int)TreeSpell.Rejuvenation].MultiplyPeriodic(defaultCritMultiplier); spells[(int)TreeSpell.Swiftmend].RaidAction.Direct *= raidLivingSeedCritMultiplier; spells[(int)TreeSpell.Swiftmend].TankAction.Direct *= tankLivingSeedCritMultiplier; spells[(int)TreeSpell.Swiftmend].MultiplyPeriodic(defaultCritMultiplier); spells[(int)TreeSpell.HealingTouch].RaidAction.Direct *= raidLivingSeedCritMultiplier; spells[(int)TreeSpell.HealingTouch].TankAction.Direct *= tankLivingSeedCritMultiplier; spells[(int)TreeSpell.Nourish].RaidAction.Direct *= raidLivingSeedCritMultiplier; spells[(int)TreeSpell.Nourish].TankAction.Direct *= tankLivingSeedCritMultiplier; spells[(int)TreeSpell.Regrowth].RaidAction.Direct *= getCritMultiplier(stats, regrowthExtraCrit, raidLivingSeed); spells[(int)TreeSpell.Regrowth].TankAction.Direct *= getCritMultiplier(stats, regrowthExtraCrit, tankLivingSeed); spells[(int)TreeSpell.Regrowth].MultiplyPeriodic(getCritMultiplier(stats, regrowthExtraCrit, 0)); #endregion #region Nourish HoTs spells[(int)TreeSpell.Nourish].RaidAction.Direct *= 1.0 + opts.NourishHoTRate * 0.2; spells[(int)TreeSpell.Nourish].TankAction.Direct *= 1.2; #endregion }
/* Spell data is computed in the following stages: * 1. SpellData with constants * 2. ComputedSpell containing computed data for a generic target (thus excluding crit, mastery, targets, effective healing) * 3. ComputedSpell with RaidAction and TankAction filled with information except effective healing and multiplication by number of targets * 4. SpellAction adding effective healing, targets and strange options, plus special SpellActions containing combination of spells * 5. Actions containing the SpellActions converted to EPS/MPS/time format * * Note that Tank mostly means "target always with hots, hit frequently" while Raid means "target which might have hots (from wild growth), hit rarely" */ ComputedSpell[] computeDivisionSpells(TreeStats stats, TreeComputedData data) { ComputedSpell[] spells = new ComputedSpell[(int)TreeSpell.Count]; for (int i = 0; i < (int)TreeSpell.Count; ++i) { SpellData spdata = CalculationsTree.SpellData[i]; if(i == (int)TreeSpell.WildGrowth && opts.WildGrowthNerf) spdata = CalculationsTree.NerfedWildGrowthSpellData; spells[i] = new ComputedSpell(spdata, stats); } spells[(int)TreeSpell.Tranquility].DirectMultiplier *= 4 * 5; spells[(int)TreeSpell.Tranquility].TickMultiplier *= 4 * 5; #region Talents double rejuvenationInstantTicks = 0; spells[(int)TreeSpell.Swiftmend].ExtraDirectBonus += 0.02f * Talents.Genesis; spells[(int)TreeSpell.Rejuvenation].ExtraTickBonus += Talents.BlessingOfTheGrove * 0.02f; spells[(int)TreeSpell.Nourish].TimeReductionMS += 250 * Talents.Naturalist; spells[(int)TreeSpell.HealingTouch].TimeReductionMS += 250 * Talents.Naturalist; spells[(int)TreeSpell.Rejuvenation].ExtraTickBonus += 0.05f * Talents.ImprovedRejuvenation; spells[(int)TreeSpell.Swiftmend].ExtraTickBonus += 0.05f * Talents.ImprovedRejuvenation; spells[(int)TreeSpell.Swiftmend].ExtraDirectBonus += 0.05f * Talents.ImprovedRejuvenation; // according to Paragon's Anaram posting on ElitistJerks, Efflorescence double-dips Master Shapeshifter and Harmony spells[(int)TreeSpell.Swiftmend].TickMultiplier *= 0.04f * Talents.Efflorescence * (1.0f + Talents.MasterShapeshifter * 0.04f) * (1.0f + stats.Harmony); spells[(int)TreeSpell.Nourish].ExtraDirectBonus += 0.05f * Talents.EmpoweredTouch; spells[(int)TreeSpell.HealingTouch].ExtraDirectBonus += 0.05f * Talents.EmpoweredTouch; spells[(int)TreeSpell.Regrowth].ExtraDirectBonus += 0.05f * Talents.EmpoweredTouch; // formula from TreeCalcs // TODO: test this in-game rejuvenationInstantTicks = Talents.GiftOfTheEarthmother * 0.05f * Math.Floor(4.0f * (1 + StatConversion.GetSpellHasteFromRating((float)stats.Haste.HasteRating)) + 0.5f) * 1.0135f; spells[(int)TreeSpell.Lifebloom].DirectMultiplier *= 1 + Talents.GiftOfTheEarthmother * 0.05f; if (Talents.SwiftRejuvenation > 0) spells[(int)TreeSpell.Rejuvenation].TimeReductionMS = 500; #endregion #region Glyphs if (Talents.GlyphOfRejuvination) spells[(int)TreeSpell.Rejuvenation].ExtraTickBonus += 0.1f; if (Talents.GlyphOfRegrowth) spells[(int)TreeSpell.Regrowth].ExtraDurationMS += (Talents.GlyphOfRegrowth ? (int)(Math.Min(opts.GlyphOfRegrowthExtraDuration, calc.FightLength) * 1000.0) : 0); #endregion for (int i = 0; i < (int)TreeSpell.Count; ++i) { if (i != (int)TreeSpell.HealingTouch && i != (int)TreeSpell.WildGrowth) spells[i].ComputeTiming(); } if (T13Count >= 4) { // here we assume that the tick structure is computed as if the spell just had double duration // this is NOT the same as doubling the amount of ticks, due to rounding // also we assume that overhealing ratio doesn't depend on duration in our model // if the model is changed to do that, then this averaging will need to be done much later for (int i = 0; i < CalculationsTree.T13Spells.Length; ++i) { ComputedSpell extspell = new ComputedSpell(CalculationsTree.T13SpellData[i], stats); extspell.ComputeTiming(); ComputedSpell spell = spells[(int)CalculationsTree.T13Spells[i]]; spell.Ticks = 0.9 * spell.Ticks + 0.1 * extspell.Ticks; spell.TPS = 0.9 * spell.TPS + 0.1 * extspell.TPS; spell.Duration = 0.9 * spell.Duration + 0.1 * extspell.Duration; } } // optimization to avoid duplicating computations spells[(int)TreeSpell.HealingTouch].Action.Time = spells[(int)TreeSpell.Nourish].Action.Time; spells[(int)TreeSpell.WildGrowth].Action.Time = spells[(int)TreeSpell.Swiftmend].Action.Time; spells[(int)TreeSpell.WildGrowth].Ticks = spells[(int)TreeSpell.Swiftmend].Ticks; spells[(int)TreeSpell.WildGrowth].Duration = spells[(int)TreeSpell.Swiftmend].Duration; spells[(int)TreeSpell.WildGrowth].TPS = spells[(int)TreeSpell.Swiftmend].TPS; for (int i = 0; i < (int)TreeSpell.Count; ++i) spells[i].ComputeRest(); if (rejuvenationInstantTicks > 0) { spells[(int)TreeSpell.Rejuvenation].Action.Direct += rejuvenationInstantTicks * spells[(int)TreeSpell.Rejuvenation].Tick; ++spells[(int)TreeSpell.Rejuvenation].Action.Ticks; } #region Cooldowns spells[(int)TreeSpell.WildGrowth].Action.Cooldown = 8 + opts.WildGrowthCastDelay; spells[(int)TreeSpell.Tranquility].Action.Cooldown = 8 * 60 - (150 * Talents.MalfurionsGift) + opts.TranquilityCastDelay; spells[(int)TreeSpell.Swiftmend].Action.Cooldown = 15 + opts.SwiftmendCastDelay; if (opts.GlyphOfWildGrowthCDIncrease && character.DruidTalents.GlyphOfWildGrowth) spells[(int)TreeSpell.WildGrowth].Action.Cooldown += 2; #endregion return spells; }
// cast Lifebloom on raid during ToL, then an amortized Healing Touch using the average number of clearcasts from the Lifebloom DiscreteAction buildTolLbCcHt(TreeStats stats, double lifebloomTicks, DiscreteAction tollb, double tollbWeight, DiscreteAction ccht) { double cchtsPerLifebloom = lifebloomTicks * 0.02f * Talents.MalfurionsGift; DiscreteAction action = new DiscreteAction(); action.Time = tollb.Time + cchtsPerLifebloom * ccht.Time; action.Direct = tollb.Direct * tollbWeight + cchtsPerLifebloom * ccht.Direct; action.Periodic = tollb.Periodic * tollbWeight; action.Mana = tollb.Mana; action.Casts = cchtsPerLifebloom; action.Ticks = lifebloomTicks + cchtsPerLifebloom; return action; }
// cast Rejuvenation rjn times to get Nature's Bounty, then cast Nourish until it drops off // TODO: do we have some extra time before it drops off? DiscreteAction buildNourishNB(TreeStats stats, DiscreteAction rj, double rjduration, int rjn, double rjWeight, DiscreteAction nourish) { DiscreteAction action = new DiscreteAction(); double nourishCasts = (rjduration - rj.Time * rjn) / ((1.0f - 0.1f * Talents.NaturesBounty) * nourish.Time); action.Time = rjduration; action.Periodic = rj.Periodic * rjn * rjWeight; action.Direct = rj.Direct * rjn * rjWeight + nourishCasts * nourish.Direct; action.Mana = rj.Mana * rjn + nourishCasts * nourish.Mana; action.Casts = rj.Casts * rjn + nourishCasts; action.Ticks = rj.Ticks * rjn; return action; }
double getCritMultiplier(TreeStats stats, double extraCritChance, double livingSeed) { double crit = Math.Min(1, stats.SpellCrit + extraCritChance); return crit * 2.0 * (1.0 + stats.BonusCritHealMultiplier) * (1 + livingSeed) + (1 - crit); }