public override Stats GetCharacterStats(Character character, Item additionalItem) { CalculationOptionsCat calcOpts = character.CalculationOptions as CalculationOptionsCat ?? new CalculationOptionsCat(); DruidTalents talents = character.DruidTalents; BossOptions bossOpts = character.BossOptions; bool hasCritBuff = false; foreach (Buff buff in character.ActiveBuffs) { if (buff.Group == "Critical Strike Chance") { hasCritBuff = true; break; } } StatsCat statsTotal = new StatsCat() { BonusAgilityMultiplier = Character.ValidateArmorSpecialization(character, ItemType.Leather) ? 0.05f : 0f, BonusAttackPowerMultiplier = (1f + 0.25f) * (1f + talents.HeartOfTheWild * 0.1f / 3f) - 1f, //BonusBleedDamageMultiplier = (character.ActiveBuffsConflictingBuffContains("Bleed Damage") ? 0f : 0.3f), MovementSpeed = 0.15f * talents.FeralSwiftness, RavageCritChanceOnTargetsAbove80Percent = 0.25f * talents.PredatoryStrikes, FurySwipesChance = 0.05f * talents.FurySwipes, CPOnCrit = 0.5f * talents.PrimalFury, FerociousBiteDamageMultiplier = 0.05f* talents.FeralAggression, EnergyOnTigersFury = 20f * talents.KingOfTheJungle, FreeRavageOnFeralChargeChance = 0.5f * talents.Stampede, PhysicalCrit = (hasCritBuff ? 0f : 0.05f * talents.LeaderOfThePack) + (talents.MasterShapeshifter == 1 ? 0.04f : 0f), MaxEnergyOnTigersFuryBerserk = 10f * talents.PrimalMadness, RipRefreshChanceOnFerociousBiteOnTargetsBelow25Percent = 0.5f * talents.BloodInTheWater, ShredDamageMultiplier = talents.RendAndTear * 0.2f / 3f, BonusBerserkDuration = (talents.Berserk > 0 ? 15f + (talents.GlyphOfBerserk ? 10f : 0f) : 0f), MangleDamageMultiplier = talents.GlyphOfMangle ? 0.1f : 0f, SavageRoarDamageMultiplierIncrease = talents.GlyphOfSavageRoar ? 0.05f : 0f, FeralChargeCatCooldownReduction = talents.GlyphOfFeralCharge ? 2f : 0f, // FerociousBiteMaxExtraEnergyReduction = talents.GlyphOfFerociousBite ? 25f : 0f, }; #region Set Bonuses int PvPCount; character.SetBonusCount.TryGetValue("Gladiator's Sanctuary", out PvPCount); if (PvPCount >= 2) { statsTotal.Agility += 70f; statsTotal.Resilience += 400f; } if (PvPCount >= 4) { // the 15% movement speed is only outdoors which most dungeons are not statsTotal.Agility += 90f; } int T11Count; character.SetBonusCount.TryGetValue("Stormrider's Battlegarb", out T11Count); if (T11Count >= 2) { statsTotal.BonusDamageMultiplierRakeTick = (1f + statsTotal.BonusDamageMultiplierRakeTick) * (1f + 0.10f) - 1f; statsTotal.BonusDamageMultiplierLacerate = (1f + statsTotal.BonusDamageMultiplierLacerate) * (1f + 0.10f) - 1f; } if (T11Count >= 4) { statsTotal.AddSpecialEffect(new SpecialEffect(Trigger.MangleCatAttack, new Stats() { BonusAttackPowerMultiplier = 0.01f, }, 30, 0, 1f, 3)); statsTotal.Tier_11_4pc = true; //statsTotal.BonusSurvivalInstinctsDurationMultiplier = (1f + statsTotal.BonusSurvivalInstinctsDurationMultiplier) * (1f + 0.50f) - 1f; } int T12Count; character.SetBonusCount.TryGetValue("Obsidian Arborweave Battlegarb", out T12Count); if (T12Count >= 2) { statsTotal.MangleDamageMultiplier = (1f + statsTotal.MangleDamageMultiplier) * (1f + 0.10f) - 1f; statsTotal.ShredDamageMultiplier = (1f + statsTotal.ShredDamageMultiplier) * (1f + 0.10f) - 1f; } if (T12Count >= 4) { // Assume that all Finishing Moves are used at 5 combo points // SpecialEffect primary = new SpecialEffect(Trigger.Berserk, new StatsCat(), statsTotal.BonusBerserkDuration, 180f); // SpecialEffect secondary = new SpecialEffect(Trigger.FinishingMove, // new StatsCat() { BonusBerserkDuration = 2f, }, // 0, 5f, 1f); // primary.Stats.AddSpecialEffect(secondary); // statsTotal.AddSpecialEffect(primary); statsTotal.Tier_12_4pc = true; } statsTotal.Tier_13_2_piece = false; statsTotal.Tier_13_4_piece = false; int T13Count; character.SetBonusCount.TryGetValue("Deep Earth Battlegarb", out T13Count); if (T13Count >= 2) { // Your Blood in the Water talent now causes Ferocious Bite to refresh the duration of your Rip on targets with 60% or less health. statsTotal.Tier_13_2_piece = true; } if (T13Count >= 4) { // Your Stampede talent now grants two charges after using Feral Charge (Cat). statsTotal.Tier_13_4_piece = true; } #endregion statsTotal.Accumulate(BaseStats.GetBaseStats(character.Level, character.Class, character.Race, BaseStats.DruidForm.Cat)); statsTotal.Accumulate(GetItemStats(character, additionalItem)); AccumulateBuffsStats(statsTotal, character.ActiveBuffs); statsTotal.Stamina = (float)Math.Floor(statsTotal.Stamina * (1f + statsTotal.BonusStaminaMultiplier)); statsTotal.Strength = (float)Math.Floor(statsTotal.Strength * (1f + statsTotal.BonusStrengthMultiplier)); statsTotal.Agility = (float)Math.Floor(statsTotal.Agility * (1f + statsTotal.BonusAgilityMultiplier)); statsTotal.AttackPower += statsTotal.Strength * 1f + (statsTotal.Agility * 2f - 20f); //-20 to account for the first 20 str and first 20 agi only giving 1ap each statsTotal.AttackPower = (float)Math.Floor(statsTotal.AttackPower * (1f + statsTotal.BonusAttackPowerMultiplier)); statsTotal.Health += (float)Math.Floor((statsTotal.Stamina - 20f) * 14f + 20f); statsTotal.Health = (float)Math.Floor(statsTotal.Health * (1f + statsTotal.BonusHealthMultiplier)); statsTotal.Armor = (float)Math.Floor(statsTotal.Armor * (1f + statsTotal.BonusArmorMultiplier)); statsTotal.NatureResistance += statsTotal.NatureResistanceBuff; statsTotal.FireResistance += statsTotal.FireResistanceBuff; statsTotal.FrostResistance += statsTotal.FrostResistanceBuff; statsTotal.ShadowResistance += statsTotal.ShadowResistanceBuff; statsTotal.ArcaneResistance += statsTotal.ArcaneResistanceBuff; int targetLevel = bossOpts.Level; float hasteBonus = StatConversion.GetPhysicalHasteFromRating(statsTotal.HasteRating, CharacterClass.Druid); hasteBonus = (1f + hasteBonus) * (1f + statsTotal.PhysicalHaste) - 1f; float meleeHitInterval = 1f / ((1f + hasteBonus) + 1f / (3.5f / hasteBonus)); float hitBonus = StatConversion.GetPhysicalHitFromRating(statsTotal.HitRating) + statsTotal.PhysicalHit; float expertiseBonus = StatConversion.GetDodgeParryReducFromExpertise(StatConversion.GetExpertiseFromRating(statsTotal.ExpertiseRating, CharacterClass.Druid) + statsTotal.Expertise, CharacterClass.Druid); float chanceDodge = Math.Max(0f, StatConversion.WHITE_DODGE_CHANCE_CAP[targetLevel - 85] - expertiseBonus); float chanceParry = 0f; float chanceMiss = Math.Max(0f, StatConversion.WHITE_MISS_CHANCE_CAP[targetLevel - 85] - hitBonus); float chanceAvoided = chanceMiss + chanceDodge + chanceParry; float rawChanceCrit = StatConversion.GetPhysicalCritFromRating(statsTotal.CritRating) + StatConversion.GetPhysicalCritFromAgility(statsTotal.Agility, CharacterClass.Druid) + statsTotal.PhysicalCrit + StatConversion.NPC_LEVEL_CRIT_MOD[targetLevel - 85]; float chanceCrit = rawChanceCrit * (1f - chanceAvoided); float chanceHit = 1f - chanceAvoided; bool usesMangle = (!character.ActiveBuffsContains("Mangle") && !character.ActiveBuffsContains("Trauma")); Dictionary<Trigger, float> triggerIntervals = new Dictionary<Trigger, float>(); Dictionary<Trigger, float> triggerChances = new Dictionary<Trigger, float>(); triggerIntervals[Trigger.Use] = 0f; triggerIntervals[Trigger.MeleeAttack] = meleeHitInterval; triggerIntervals[Trigger.MeleeHit] = meleeHitInterval; triggerIntervals[Trigger.PhysicalHit] = meleeHitInterval; triggerIntervals[Trigger.PhysicalAttack] = meleeHitInterval; triggerIntervals[Trigger.MeleeCrit] = meleeHitInterval; triggerIntervals[Trigger.PhysicalCrit] = meleeHitInterval; triggerIntervals[Trigger.DoTTick] = 1.5f; triggerIntervals[Trigger.DamageDone] = meleeHitInterval / 2f; triggerIntervals[Trigger.DamageOrHealingDone] = meleeHitInterval / 2f; // Need to Add Self-Heals triggerIntervals[Trigger.RakeTick] = 3f; if (usesMangle) { triggerIntervals[Trigger.MangleCatHit] = 60f; triggerIntervals[Trigger.MangleCatAttack] = 60f; } triggerIntervals[Trigger.MangleCatOrShredHit] = usesMangle ? 3.76f : 3.87f; triggerIntervals[Trigger.MangleCatOrShredOrInfectedWoundsHit] = triggerIntervals[Trigger.MangleCatOrShredHit] / ((talents.InfectedWounds > 0) ? 2f : 1f); triggerIntervals[Trigger.EnergyOrFocusDropsBelow20PercentOfMax] = 4f; // doing 80% chance every 4 seconds per Astry triggerIntervals[Trigger.FinishingMove] = 9f; // Assume it takes 9 seconds between to perform a finishing move triggerIntervals[Trigger.Berserk] = 180f; triggerChances[Trigger.Use] = 1f; triggerChances[Trigger.MeleeAttack] = 1f; triggerChances[Trigger.MeleeHit] = Math.Max(0f, chanceHit); triggerChances[Trigger.PhysicalHit] = Math.Max(0f, chanceHit); triggerChances[Trigger.PhysicalAttack] = 1f; triggerChances[Trigger.MeleeCrit] = Math.Max(0f, chanceCrit); triggerChances[Trigger.PhysicalCrit] = Math.Max(0f, chanceCrit); triggerChances[Trigger.DoTTick] = 1f; triggerChances[Trigger.DamageDone] = 1f - chanceAvoided / 2f; triggerChances[Trigger.DamageOrHealingDone] = 1f - chanceAvoided / 2f; // Need to Add Self-Heals triggerChances[Trigger.RakeTick] = 1f; if (usesMangle) { triggerChances[Trigger.MangleCatAttack] = 1f; triggerChances[Trigger.MangleCatHit] = chanceHit; } triggerChances[Trigger.MangleCatOrShredHit] = chanceHit; triggerChances[Trigger.MangleCatOrShredOrInfectedWoundsHit] = chanceHit; triggerChances[Trigger.EnergyOrFocusDropsBelow20PercentOfMax] = 0.80f; // doing 80% chance every 4 seconds per Astry triggerChances[Trigger.FinishingMove] = 1f; triggerChances[Trigger.Berserk] = 1f; // Handle Trinket procs Stats statsProcs = new Stats(); foreach (SpecialEffect effect in statsTotal.SpecialEffects(se => triggerIntervals.ContainsKey(se.Trigger))) { // JOTHAY's NOTE: The following is an ugly hack to add Recursive Effects to Cat // so Victor's Call and similar trinkets can be given more appropriate value if (effect.Trigger == Trigger.Use && effect.Stats._rawSpecialEffectDataSize == 1 && triggerIntervals.ContainsKey(effect.Stats._rawSpecialEffectData[0].Trigger)) { float upTime = effect.GetAverageUptime(triggerIntervals[effect.Trigger], triggerChances[effect.Trigger], 1f, bossOpts.BerserkTimer); statsProcs.Accumulate(effect.Stats._rawSpecialEffectData[0].GetAverageStats( triggerIntervals[effect.Stats._rawSpecialEffectData[0].Trigger], triggerChances[effect.Stats._rawSpecialEffectData[0].Trigger], 1f, bossOpts.BerserkTimer), upTime); } else if (effect.Stats.MoteOfAnger > 0) { // When in effect stats, MoteOfAnger is % of melee hits // When in character stats, MoteOfAnger is average procs per second statsProcs.MoteOfAnger = effect.Stats.MoteOfAnger * effect.GetAverageProcsPerSecond(triggerIntervals[effect.Trigger], triggerChances[effect.Trigger], 1f, bossOpts.BerserkTimer) / effect.MaxStack; } else { statsProcs.Accumulate(effect.GetAverageStats(triggerIntervals[effect.Trigger], triggerChances[effect.Trigger], 1f, bossOpts.BerserkTimer)); } } statsProcs.Agility += statsProcs.HighestStat + statsProcs.Paragon; statsProcs.Stamina = (float)Math.Floor(statsProcs.Stamina * (1f + statsTotal.BonusStaminaMultiplier)); statsProcs.Strength = (float)Math.Floor(statsProcs.Strength * (1f + statsTotal.BonusStrengthMultiplier)); statsProcs.Agility = (float)Math.Floor(statsProcs.Agility * (1f + statsTotal.BonusAgilityMultiplier)); statsProcs.AttackPower += statsProcs.Strength * 1f + statsProcs.Agility * 2f; statsProcs.AttackPower = (float)Math.Floor(statsProcs.AttackPower * (1f + statsTotal.BonusAttackPowerMultiplier)); statsProcs.Health += (float)Math.Floor(statsProcs.Stamina * 10f); //statsProcs.Armor += 2f * statsProcs.Agility; statsProcs.Armor = (float)Math.Floor(statsProcs.Armor * (1f + statsTotal.BonusArmorMultiplier)); if (statsProcs.HighestSecondaryStat > 0) { if (statsTotal.CritRating > statsTotal.HasteRating && statsTotal.CritRating > statsTotal.MasteryRating) { statsProcs.CritRating += statsProcs.HighestSecondaryStat; // this will be invalidated after this, but I'm at least putting it in for now } else if (statsTotal.HasteRating > statsTotal.CritRating && statsTotal.HasteRating > statsTotal.MasteryRating) { statsProcs.HasteRating += statsProcs.HighestSecondaryStat; } else if (statsTotal.MasteryRating > statsTotal.CritRating && statsTotal.MasteryRating > statsTotal.HasteRating) { statsProcs.MasteryRating += statsProcs.HighestSecondaryStat; } statsProcs.HighestSecondaryStat = 0; } //Agility is only used for crit from here on out; we'll be converting Agility to CritRating, //and calculating CritRating separately, so don't add any Agility or CritRating from procs here. statsProcs.CritRating = statsProcs.Agility = 0; statsTotal.Accumulate(statsProcs); //Handle Crit procs statsTotal.TemporaryCritRatingUptimes = new WeightedStat[0]; List<SpecialEffect> tempCritEffects = new List<SpecialEffect>(); List<float> tempCritEffectIntervals = new List<float>(); List<float> tempCritEffectChances = new List<float>(); List<float> tempCritEffectScales = new List<float>(); foreach (SpecialEffect effect in statsTotal.SpecialEffects(se => triggerIntervals.ContainsKey(se.Trigger) && (se.Stats.CritRating + se.Stats.Agility + se.Stats.HighestStat + se.Stats.Paragon) > 0)) { tempCritEffects.Add(effect); tempCritEffectIntervals.Add(triggerIntervals[effect.Trigger]); tempCritEffectChances.Add(triggerChances[effect.Trigger]); tempCritEffectScales.Add(1f); } if (tempCritEffects.Count == 0) { statsTotal.TemporaryCritRatingUptimes = new WeightedStat[] { new WeightedStat() { Chance = 1f, Value = 0f } }; } else if (tempCritEffects.Count == 1) { //Only one, add it to SpecialEffect effect = tempCritEffects[0]; float uptime = effect.GetAverageUptime(triggerIntervals[effect.Trigger], triggerChances[effect.Trigger], 1f, bossOpts.BerserkTimer) * tempCritEffectScales[0]; float totalAgi = (float)effect.MaxStack * (effect.Stats.Agility + effect.Stats.HighestStat + effect.Stats.Paragon) * (1f + statsTotal.BonusAgilityMultiplier); statsTotal.TemporaryCritRatingUptimes = new WeightedStat[] { new WeightedStat() { Chance = uptime, Value = effect.Stats.CritRating + StatConversion.GetCritFromAgility(totalAgi, CharacterClass.Druid) * StatConversion.RATING_PER_PHYSICALCRIT }, new WeightedStat() { Chance = 1f - uptime, Value = 0f }}; } else if (tempCritEffects.Count > 1) { List<float> tempCritEffectsValues = new List<float>(); foreach (SpecialEffect effect in tempCritEffects) { float totalAgi = (float)effect.MaxStack * (effect.Stats.Agility + effect.Stats.HighestStat + effect.Stats.Paragon) * (1f + statsTotal.BonusAgilityMultiplier); tempCritEffectsValues.Add(effect.Stats.CritRating + StatConversion.GetCritFromAgility(totalAgi, CharacterClass.Druid) * StatConversion.RATING_PER_PHYSICALCRIT); } float[] intervals = new float[tempCritEffects.Count]; float[] chances = new float[tempCritEffects.Count]; float[] offset = new float[tempCritEffects.Count]; for (int i = 0; i < tempCritEffects.Count; i++) { intervals[i] = triggerIntervals[tempCritEffects[i].Trigger]; chances[i] = triggerChances[tempCritEffects[i].Trigger]; } if (tempCritEffects.Count >= 2) { offset[0] = calcOpts.TrinketOffset; } WeightedStat[] critWeights = SpecialEffect.GetAverageCombinedUptimeCombinations(tempCritEffects.ToArray(), intervals, chances, offset, tempCritEffectScales.ToArray(), 1f, bossOpts.BerserkTimer, tempCritEffectsValues.ToArray()); statsTotal.TemporaryCritRatingUptimes = critWeights; } return statsTotal; }
public Stats GetBuffsStats(Character character, CalculationOptionsCat calcOpts) { List<Buff> removedBuffs = new List<Buff>(); List<Buff> addedBuffs = new List<Buff>(); List<Buff> buffGroup = new List<Buff>(); #region Maintenance Auto-Fixing // Removes the Sunder Armor if you are maintaining it yourself // Also removes Acid Spit and Expose Armor // We are now calculating this internally for better accuracy and to provide value to relevant talents if (calcOpts.CustomUseMangle) { buffGroup.Clear(); buffGroup.Add(Buff.GetBuffByName("Sunder Armor")); buffGroup.Add(Buff.GetBuffByName("Expose Armor")); buffGroup.Add(Buff.GetBuffByName("Faerie Fire")); buffGroup.Add(Buff.GetBuffByName("Corrosive Spit")); buffGroup.Add(Buff.GetBuffByName("Tear Armor")); MaintBuffHelper(buffGroup, character, removedBuffs); } #endregion StatsCat statsBuffs = new StatsCat(); statsBuffs.Accumulate(GetBuffsStats(character.ActiveBuffs, character.SetBonusCount)); foreach (Buff b in removedBuffs) { character.ActiveBuffsAdd(b); } foreach (Buff b in addedBuffs) { character.ActiveBuffs.Remove(b); } return statsBuffs; }