From 03f4646441f9ea8326680926ff1154eb858bec1e Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Sat, 30 Sep 2023 11:15:29 +0200 Subject: [PATCH 01/13] Move auto gem to shared and make it generic --- ui/feral_druid/sim.ts | 351 +--------------------------- ui/hunter/sim.ts | 12 +- ui/shared/auto_gem.ts | 520 ++++++++++++++++++++++++++++++++++++++++++ ui/warrior/sim.ts | 293 +----------------------- 4 files changed, 532 insertions(+), 644 deletions(-) create mode 100644 ui/shared/auto_gem.ts diff --git a/ui/feral_druid/sim.ts b/ui/feral_druid/sim.ts index 00bb240027..7218566b8d 100644 --- a/ui/feral_druid/sim.ts +++ b/ui/feral_druid/sim.ts @@ -8,18 +8,13 @@ import { TristateEffect } from '../core/proto/common.js' import { Stats } from '../core/proto_utils/stats.js'; import { Player } from '../core/player.js'; import { IndividualSimUI } from '../core/individual_sim_ui.js'; -import { EventID, TypedEvent } from '../core/typed_event.js'; -import { Gear } from '../core/proto_utils/gear.js'; -import { ItemSlot } from '../core/proto/common.js'; -import { GemColor } from '../core/proto/common.js'; -import { Profession } from '../core/proto/common.js'; import * as IconInputs from '../core/components/icon_inputs.js'; import * as OtherInputs from '../core/components/other_inputs.js'; -import * as Tooltips from '../core/constants/tooltips.js'; import * as DruidInputs from './inputs.js'; import * as Presets from './presets.js'; +import { optimizeGems } from '../shared/auto_gem.js'; export class FeralDruidSimUI extends IndividualSimUI { constructor(parentElem: HTMLElement, player: Player) { @@ -169,349 +164,7 @@ export class FeralDruidSimUI extends IndividualSimUI { addOptimizeGemsAction() { this.addAction('Suggest Gems', 'optimize-gems-action', async () => { - this.optimizeGems(); + optimizeGems(this.sim, this.player); }); } - - async optimizeGems() { - // First, clear all existing gems - let optimizedGear = this.player.getGear().withoutGems(); - - // Next, socket the meta - optimizedGear = optimizedGear.withMetaGem(this.sim.db.lookupGem(41398)); - - // Next, socket a Nightmare Tear in the best blue socket bonus - const epWeights = this.player.getEpWeights(); - let tearColor = GemColor.GemColorBlue; - let tearSlot = this.findBlueTearSlot(optimizedGear, epWeights); - - if (tearSlot == null) { - tearColor = GemColor.GemColorYellow; - tearSlot = this.findYellowTearSlot(optimizedGear, epWeights); - } - - optimizedGear = this.socketTear(optimizedGear, tearSlot, tearColor); - await this.updateGear(optimizedGear); - - // Next, identify all sockets where red gems will be placed - const redSockets = this.findSocketsByColor(optimizedGear, epWeights, GemColor.GemColorRed, tearSlot); - - // Rank order red gems to use with their associated stat caps - const redGemCaps = new Array<[number, Stats]>(); - const arpTarget = this.calcArpTarget(optimizedGear); - const arpCap = new Stats().withStat(Stat.StatArmorPenetration, arpTarget + 11); - redGemCaps.push([40117, arpCap]); - const expCap = new Stats().withStat(Stat.StatExpertise, 6.5 * 32.79 + 4); - redGemCaps.push([40118, expCap]); - const critCap = this.calcCritCap(optimizedGear); - redGemCaps.push([40112, critCap]); - redGemCaps.push([40111, new Stats()]); - - // If JC, then socket 34 ArP gems in first three red sockets before proceeding - let startIdx = 0; - - if (this.player.hasProfession(Profession.Jewelcrafting)) { - optimizedGear = this.optimizeJcGems(optimizedGear, redSockets, arpTarget, arpCap, critCap); - startIdx = 3; - } - - // Do multiple passes to fill in red gems up their caps - optimizedGear = await this.fillGemsToCaps(optimizedGear, redSockets, redGemCaps, 0, startIdx); - - // Now repeat the process for yellow gems - const yellowSockets = this.findSocketsByColor(optimizedGear, epWeights, GemColor.GemColorYellow, tearSlot); - const yellowGemCaps = new Array<[number, Stats]>(); - const hitCap = new Stats().withStat(Stat.StatMeleeHit, 8. * 32.79 + 4); - yellowGemCaps.push([40125, hitCap]); - yellowGemCaps.push([40162, hitCap.add(expCap)]); - - // If a hard ArP stack configuration is detected, then allow for socketing ArP gems in weaker yellow sockets after capping Hit and Expertise - if (this.detectArpStackConfiguration(arpTarget)) { - this.sortYellowSockets(optimizedGear, yellowSockets, epWeights, tearSlot); - yellowGemCaps.reverse(); - yellowGemCaps.push([40117, arpCap]); - } - - // Continue with the rest of the yellow gems otherwise - yellowGemCaps.push([40148, hitCap.add(critCap)]); - yellowGemCaps.push([40143, hitCap]); - yellowGemCaps.push([40147, critCap]); - yellowGemCaps.push([40142, critCap]); - yellowGemCaps.push([40146, new Stats()]); - await this.fillGemsToCaps(optimizedGear, yellowSockets, yellowGemCaps, 0, 0); - } - - calcArpTarget(gear: Gear): number { - let arpTarget = 1399; - - // First handle ArP proc trinkets - if (gear.hasTrinket(45931)) { - arpTarget -= 751; - } else if (gear.hasTrinket(40256)) { - arpTarget -= 612; - } - - // Then check for Executioner enchant - const weapon = gear.getEquippedItem(ItemSlot.ItemSlotMainHand); - - if ((weapon != null) && (weapon!.enchant != null) && (weapon!.enchant!.effectId == 3225)) { - arpTarget -= 120; - } - - return arpTarget; - } - - calcCritCap(gear: Gear): Stats { - const baseCritCapPercentage = 77.8; // includes 3% Crit debuff - let agiProcs = 0; - - if (gear.hasRelic(47668)) { - agiProcs += 200; - } - - if (gear.hasRelic(50456)) { - agiProcs += 44*5; - } - - if (gear.hasTrinket(47131) || gear.hasTrinket(47464)) { - agiProcs += 510; - } - - if (gear.hasTrinket(47115) || gear.hasTrinket(47303)) { - agiProcs += 450; - } - - if (gear.hasTrinket(44253) || gear.hasTrinket(42987)) { - agiProcs += 300; - } - - return new Stats().withStat(Stat.StatMeleeCrit, (baseCritCapPercentage - agiProcs*1.1*1.06*1.02/83.33) * 45.91); - } - - async updateGear(gear: Gear): Promise { - this.player.setGear(TypedEvent.nextEventID(), gear); - await this.sim.updateCharacterStats(TypedEvent.nextEventID()); - return Stats.fromProto(this.player.getCurrentStats().finalStats); - } - - findBlueTearSlot(gear: Gear, epWeights: Stats): ItemSlot | null { - let tearSlot: ItemSlot | null = null; - let maxBlueSocketBonusEP: number = 1e-8; - - for (var slot of gear.getItemSlots()) { - const item = gear.getEquippedItem(slot); - - if (!item) { - continue; - } - - if (item!.numSocketsOfColor(GemColor.GemColorBlue) != 1) { - continue; - } - - const socketBonusEP = new Stats(item.item.socketBonus).computeEP(epWeights); - - if (socketBonusEP > maxBlueSocketBonusEP) { - tearSlot = slot; - maxBlueSocketBonusEP = socketBonusEP; - } - } - - return tearSlot; - } - - findYellowTearSlot(gear: Gear, epWeights: Stats): ItemSlot | null { - let tearSlot: ItemSlot | null = null; - let maxYellowSocketBonusEP: number = 1e-8; - - for (var slot of gear.getItemSlots()) { - const item = gear.getEquippedItem(slot); - - if (!item) { - continue; - } - - if (item!.numSocketsOfColor(GemColor.GemColorBlue) != 0) { - continue; - } - - const numYellowSockets = item!.numSocketsOfColor(GemColor.GemColorYellow); - - if (numYellowSockets == 0) { - continue; - } - - const socketBonusEP = new Stats(item.item.socketBonus).computeEP(epWeights); - const normalizedEP = socketBonusEP / numYellowSockets; - - if (normalizedEP > maxYellowSocketBonusEP) { - tearSlot = slot; - maxYellowSocketBonusEP = normalizedEP; - } - } - - return tearSlot; - } - - socketTear(gear: Gear, tearSlot: ItemSlot | null, tearColor: GemColor): Gear { - if (tearSlot != null) { - const tearSlotItem = gear.getEquippedItem(tearSlot); - - for (const [socketIdx, socketColor] of tearSlotItem!.allSocketColors().entries()) { - if (socketColor == tearColor) { - return gear.withEquippedItem(tearSlot, tearSlotItem!.withGem(this.sim.db.lookupGem(49110), socketIdx), true); - } - } - } - - return gear; - } - - findSocketsByColor(gear: Gear, epWeights: Stats, color: GemColor, tearSlot: ItemSlot | null): Array<[ItemSlot, number]> { - const socketList = new Array<[ItemSlot, number]>(); - const isBlacksmithing = this.player.isBlacksmithing(); - - for (var slot of gear.getItemSlots()) { - const item = gear.getEquippedItem(slot); - - if (!item) { - continue; - } - - const ignoreYellowSockets = ((item!.numSocketsOfColor(GemColor.GemColorBlue) > 0) && (slot != tearSlot)) - - for (const [socketIdx, socketColor] of item!.curSocketColors(isBlacksmithing).entries()) { - if (item!.hasSocketedGem(socketIdx)) { - continue; - } - - let matchYellowSocket = false; - - if ((socketColor == GemColor.GemColorYellow) && !ignoreYellowSockets) { - matchYellowSocket = new Stats(item.item.socketBonus).computeEP(epWeights) > 1e-8; - } - - if (((color == GemColor.GemColorYellow) && matchYellowSocket) || ((color == GemColor.GemColorRed) && !matchYellowSocket)) { - socketList.push([slot, socketIdx]); - } - } - } - - return socketList; - } - - sortYellowSockets(gear: Gear, yellowSocketList: Array<[ItemSlot, number]>, epWeights: Stats, tearSlot: ItemSlot | null) { - yellowSocketList.sort((a,b) => { - // If both yellow sockets belong to the same item, then treat them equally. - const slot1 = a[0]; - const slot2 = b[0]; - - if (slot1 == slot2) { - return 0; - } - - // If an item already has a Nightmare Tear socketed, then bump up any yellow sockets in it to highest priority. - if (slot1 == tearSlot) { - return -1; - } - - if (slot2 == tearSlot) { - return 1; - } - - // For all other cases, sort by the ratio of the socket bonus value divided by the number of yellow sockets required to activate it. - const item1 = gear.getEquippedItem(slot1); - const bonus1 = new Stats(item1!.item.socketBonus).computeEP(epWeights); - const item2 = gear.getEquippedItem(slot2); - const bonus2 = new Stats(item2!.item.socketBonus).computeEP(epWeights); - return bonus2 / item2!.numSocketsOfColor(GemColor.GemColorYellow) - bonus1 / item1!.numSocketsOfColor(GemColor.GemColorYellow); - }); - } - - async fillGemsToCaps(gear: Gear, socketList: Array<[ItemSlot, number]>, gemCaps: Array<[number, Stats]>, numPasses: number, firstIdx: number): Promise { - let updatedGear: Gear = gear; - const currentGem = this.sim.db.lookupGem(gemCaps[numPasses][0]); - - // On the first pass, we simply fill all sockets with the highest priority gem - if (numPasses == 0) { - for (const [itemSlot, socketIdx] of socketList.slice(firstIdx)) { - updatedGear = updatedGear.withGem(itemSlot, socketIdx, currentGem); - } - } - - // If we are below the relevant stat cap for the gem we just filled on the last pass, then we are finished. - let newStats = await this.updateGear(updatedGear); - const currentCap = gemCaps[numPasses][1]; - - if (newStats.belowCaps(currentCap) || (numPasses == gemCaps.length - 1)) { - return updatedGear; - } - - // If we exceeded the stat cap, then work backwards through the socket list and replace each gem with the next highest priority option until we are below the cap - const nextGem = this.sim.db.lookupGem(gemCaps[numPasses + 1][0]); - const nextCap = gemCaps[numPasses + 1][1]; - let capForReplacement = currentCap.subtract(nextCap); - - if (currentCap.computeEP(capForReplacement) <= 0) { - capForReplacement = currentCap; - } - - for (var idx = socketList.length - 1; idx >= firstIdx; idx--) { - if (newStats.belowCaps(capForReplacement)) { - break; - } - - const [itemSlot, socketIdx] = socketList[idx]; - updatedGear = updatedGear.withGem(itemSlot, socketIdx, nextGem); - newStats = await this.updateGear(updatedGear); - } - - // Now run a new pass to check whether we've exceeded the next stat cap - let nextIdx = idx + 1; - - if (!newStats.belowCaps(currentCap)) { - nextIdx = firstIdx; - } - - return await this.fillGemsToCaps(updatedGear, socketList, gemCaps, numPasses + 1, nextIdx); - } - - calcDistanceToArpTarget(numJcArpGems: number, passiveArp: number, numRedSockets: number, arpCap: number, arpTarget: number): number { - const numNormalArpGems = Math.max(0, Math.min(numRedSockets - 3, Math.floor((arpCap - passiveArp - 34 * numJcArpGems) / 20))); - const projectedArp = passiveArp + 34 * numJcArpGems + 20 * numNormalArpGems; - return Math.abs(projectedArp - arpTarget); - } - - optimizeJcGems(gear: Gear, redSocketList: Array<[ItemSlot, number]>, arpTarget: number, arpCap: Stats, critCap: Stats): Gear { - const passiveStats = Stats.fromProto(this.player.getCurrentStats().finalStats); - const passiveArp = passiveStats.getStat(Stat.StatArmorPenetration); - const numRedSockets = redSocketList.length; - const arpCapValue = arpCap.getStat(Stat.StatArmorPenetration); - - // First determine how many of the JC gems should be 34 ArP gems - const optimalJcArpGems = [0,1,2,3].reduce((m,x)=> this.calcDistanceToArpTarget(m, passiveArp, numRedSockets, arpCapValue, arpTarget) 1000) && (currentArp > 648) && (currentArp + 20 < arpTarget + 11); - } } diff --git a/ui/hunter/sim.ts b/ui/hunter/sim.ts index 52a69023b1..52b0cb2d0a 100644 --- a/ui/hunter/sim.ts +++ b/ui/hunter/sim.ts @@ -20,15 +20,13 @@ import { Player } from '../core/player.js'; import { Stats } from '../core/proto_utils/stats.js'; import { getTalentPoints } from '../core/proto_utils/utils.js'; import { IndividualSimUI } from '../core/individual_sim_ui.js'; -import { EventID, TypedEvent } from '../core/typed_event.js'; +import { TypedEvent } from '../core/typed_event.js'; import { getPetTalentsConfig } from '../core/talents/hunter_pet.js'; import { protoToTalentString } from '../core/talents/factory.js'; import { - Hunter, Hunter_Rotation as HunterRotation, Hunter_Rotation_StingType as StingType, - Hunter_Options as HunterOptions, Hunter_Options_PetType as PetType, HunterPetTalents, Hunter_Rotation_RotationType, @@ -37,11 +35,11 @@ import { import * as IconInputs from '../core/components/icon_inputs.js'; import * as OtherInputs from '../core/components/other_inputs.js'; import * as Mechanics from '../core/constants/mechanics.js'; -import * as Tooltips from '../core/constants/tooltips.js'; import * as AplUtils from '../core/proto_utils/apl_utils.js'; import * as HunterInputs from './inputs.js'; import * as Presets from './presets.js'; +import { optimizeGems } from '../shared/auto_gem.js'; export class HunterSimUI extends IndividualSimUI { constructor(parentElem: HTMLElement, player: Player) { @@ -369,5 +367,11 @@ export class HunterSimUI extends IndividualSimUI { }); }, }); + this.addOptimizeGemsAction(); + } + addOptimizeGemsAction() { + this.addAction('Suggest Gems', 'optimize-gems-action', async () => { + optimizeGems(this.sim, this.player); + }); } } diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts new file mode 100644 index 0000000000..938883df79 --- /dev/null +++ b/ui/shared/auto_gem.ts @@ -0,0 +1,520 @@ +import { Player } from "../core/player"; +import { GemColor, ItemSlot, Profession, Spec, Stat } from "../core/proto/common"; +import { Gear } from "../core/proto_utils/gear"; +import { Stats } from "../core/proto_utils/stats"; +import { Sim } from "../core/sim"; +import { TypedEvent } from "../core/typed_event"; +import * as Mechanics from '../core/constants/mechanics.js'; + +/*** + * WARNING: Currently only optimised for Arp/Exp/Hit gemming the following specs; + * - Feral + * - Warrior + * - Hunter + */ +type AutoGemSpec = Spec.SpecWarrior | Spec.SpecFeralDruid | Spec.SpecHunter + +enum GemsByStats { + Str = 40111, + Agi = 40112, + Arp = 40117, + Exp = 40118, + Hit = 40125, + Str_Crit = 40142, + Str_Hit = 40143, + Str_Haste = 40146, + Agi_Crit = 40147, + Agi_Hit = 40148, + Exp_Hit = 40162, +} + +/** + * Add spec specific slop to the real ArP cap + */ +const calcArpCap = (arpTarget: number, player: Player): Stats => { + let arpCap = arpTarget + // Sets additional "slop" to allow for minor overcapping + switch (player.spec) { + case Spec.SpecFeralDruid: + arpCap += 11 + break; + default: + arpCap += 4 + break; + } + return new Stats().withStat(Stat.StatArmorPenetration, arpCap); +} + +/** + * Calculate the real ArP cap value the player needs + */ +const calcArpTarget = (gear: Gear): Stats => { + let arpCap = 1399; + + // Mjolnir Runestone + if (gear.hasTrinket(45931)) { + arpCap -= 751; + } + + // Grim Toll + if (gear.hasTrinket(40256)) { + arpCap -= 612; + } + + // Executioner enchant + const weapon = gear.getEquippedItem(ItemSlot.ItemSlotMainHand); + if (weapon?.enchant?.effectId === 3225) { + arpCap -= 120; + } + + return new Stats().withStat(Stat.StatArmorPenetration, arpCap); +} + +/** + * Calculate the value still needed to hit ArP cap + */ +const calcDistanceToArpTarget = (numJcArpGems: number, passiveArp: number, numRedSockets: number, arpCap: number, arpTarget: number): number => { + const numNormalArpGems = Math.max(0, Math.min(numRedSockets - 3, Math.floor((arpCap - passiveArp - 34 * numJcArpGems) / 20))); + const projectedArp = passiveArp + 34 * numJcArpGems + 20 * numNormalArpGems; + return Math.abs(projectedArp - arpTarget); +} + +/** + * Calculate the Expertise cap value and add spec specific slop + */ +const calcExpCap = (player: Player): Stats => { + let expCap = 6.5 * 32.79; + const talents = player.getTalents() + + switch (player.spec) { + case Spec.SpecWarrior: + if ('weaponMastery' in talents) { + const weaponMastery = talents.weaponMastery; + const hasWeaponMasteryTalent = !!weaponMastery; + + if (hasWeaponMasteryTalent) { + expCap -= + weaponMastery * 4 * Mechanics.EXPERTISE_PER_QUARTER_PERCENT_REDUCTION; + } + } + break; + } + + expCap += 4 // Add 4 as default slop + + return new Stats().withStat(Stat.StatExpertise, expCap); +} + +/** + * Calculate the Crit cap value + */ +const calcCritCap = (gear: Gear): Stats => { + const baseCritCapPercentage = 75.8 + 3; // includes 3% Crit debuff + let agiProcs = 0; + + if (gear.hasRelic(47668)) { + agiProcs += 200; + } + + if (gear.hasRelic(50456)) { + agiProcs += 44 * 5; + } + + if (gear.hasTrinket(47131) || gear.hasTrinket(47464)) { + agiProcs += 510; + } + + if (gear.hasTrinket(47115) || gear.hasTrinket(47303)) { + agiProcs += 450; + } + + if (gear.hasTrinket(44253) || gear.hasTrinket(42987)) { + agiProcs += 300; + } + + return new Stats().withStat(Stat.StatMeleeCrit, (baseCritCapPercentage - agiProcs * 1.1 * 1.06 * 1.02 / 83.33) * 45.91); +} + +/** + * Calculate the Crit value + * and add spec specific slop + */ +const calcHitCap = (player: Player): Stats => { + let hitCap = 8. * 32.79 + + // Sets additional "slop" to allow for minor overcapping + switch (player.spec) { + default: + hitCap += 11 + break; + } + + return new Stats().withStat(Stat.StatMeleeHit, hitCap) +} + +const optimizeJewelCraftingGems = (sim: Sim, player: Player, gear: Gear, redSocketList: [ItemSlot, number][], arpCap: Stats, arpTarget: number): Gear => { + const passiveStats = Stats.fromProto(player.getCurrentStats().finalStats); + const passiveArp = passiveStats.getStat(Stat.StatArmorPenetration); + const arpCapValue = arpCap.getStat(Stat.StatArmorPenetration); + const numRedSockets = redSocketList.length; + const isBelowCritCap = passiveStats.belowCaps(calcCritCap(gear)); + + // First determine how many of the JC gems should be 34 ArP gems + let optimalJcArpGems = 0; + let minDistanceToArpTarget = calcDistanceToArpTarget(0, passiveArp, numRedSockets, arpCapValue, arpTarget); + + for (let i = 1; i <= 3; i++) { + const distanceToArpTarget = calcDistanceToArpTarget(i, passiveArp, numRedSockets, arpCapValue, arpTarget); + + if (distanceToArpTarget < minDistanceToArpTarget) { + optimalJcArpGems = i; + minDistanceToArpTarget = distanceToArpTarget; + } + } + + // Now actually socket the gems + let updatedGear: Gear = gear; + for (let i = 0; i < 3; i++) { + let gemId: number | null = null; + + switch (player.spec) { + case Spec.SpecHunter: + gemId = 42143 // Agi by default + case Spec.SpecWarrior: + gemId = 42142 // Str by default + case Spec.SpecFeralDruid: + gemId = 42142 // Str by default + } + + + if (i < optimalJcArpGems) { + gemId = 42153; // ArP + } else if ((player.spec === Spec.SpecFeralDruid) && isBelowCritCap) { + gemId = 42143; // Below crit swap to Agi + } + + if (gemId) updatedGear = updatedGear.withGem(redSocketList[i][0], redSocketList[i][1], sim.db.lookupGem(gemId)); + } + + return updatedGear; +} + +const fillGemsToCaps = async (sim: Sim, player: Player, gear: Gear, socketList: Array<[ItemSlot, number]>, gemCaps: Array<[number, Stats]>, numPasses: number, firstIdx: number): Promise => { + let updatedGear: Gear = gear; + const currentGem = sim.db.lookupGem(gemCaps[numPasses][0]); + + // On the first pass, we simply fill all sockets with the highest priority gem + if (numPasses === 0) { + for (const [itemSlot, socketIdx] of socketList.slice(firstIdx)) { + updatedGear = updatedGear.withGem(itemSlot, socketIdx, currentGem); + } + } + + // If we are below the relevant stat cap for the gem we just filled on the last pass, then we are finished. + let newStats = await updateGear(sim, player, updatedGear); + const currentCap = gemCaps[numPasses][1]; + + if (newStats.belowCaps(currentCap) || (numPasses === gemCaps.length - 1)) { + return updatedGear; + } + + // If we exceeded the stat cap, then work backwards through the socket list and replace each gem with the next highest priority option until we are below the cap + const nextGem = sim.db.lookupGem(gemCaps[numPasses + 1][0]); + const nextCap = gemCaps[numPasses + 1][1]; + let capForReplacement = currentCap; + + switch (player.spec) { + case Spec.SpecFeralDruid: + capForReplacement = currentCap.subtract(nextCap); + if (currentCap.computeEP(capForReplacement) <= 0) { + capForReplacement = currentCap; + } + break; + case Spec.SpecWarrior: + case Spec.SpecHunter: + if ((numPasses > 0) && !currentCap.equals(nextCap)) { + capForReplacement = currentCap.subtract(nextCap); + } + break + } + + for (var idx = socketList.length - 1; idx >= firstIdx; idx--) { + if (newStats.belowCaps(capForReplacement)) { + break; + } + + const [itemSlot, socketIdx] = socketList[idx]; + updatedGear = updatedGear.withGem(itemSlot, socketIdx, nextGem); + newStats = await updateGear(sim, player, updatedGear); + } + + // Now run a new pass to check whether we've exceeded the next stat cap + let nextIdx = idx + 1; + + if (!newStats.belowCaps(currentCap)) { + nextIdx = firstIdx; + } + + return await fillGemsToCaps(sim, player, updatedGear, socketList, gemCaps, numPasses + 1, nextIdx); +} + +const updateGear = async (sim: Sim, player: Player, gear: Gear): Promise => { + player.setGear(TypedEvent.nextEventID(), gear); + await sim.updateCharacterStats(TypedEvent.nextEventID()); + return Stats.fromProto(player.getCurrentStats().finalStats); +} +const findBlueTearSlot = (gear: Gear, epWeights: Stats): ItemSlot | null => { + let tearSlot: ItemSlot | null = null; + let maxBlueSocketBonusEP: number = 1e-8; + + for (var slot of gear.getItemSlots()) { + const item = gear.getEquippedItem(slot); + + if (!item) { + continue; + } + + if (item.numSocketsOfColor(GemColor.GemColorBlue) !== 1) { + continue; + } + + const socketBonusEP = new Stats(item.item.socketBonus).computeEP(epWeights); + + if (socketBonusEP > maxBlueSocketBonusEP) { + tearSlot = slot; + maxBlueSocketBonusEP = socketBonusEP; + } + } + + return tearSlot; +} + +const findYellowTearSlot = (gear: Gear, epWeights: Stats): ItemSlot | null => { + let tearSlot: ItemSlot | null = null; + let maxYellowSocketBonusEP: number = 1e-8; + + for (var slot of gear.getItemSlots()) { + const item = gear.getEquippedItem(slot); + + if (!item) { + continue; + } + + if (item.numSocketsOfColor(GemColor.GemColorBlue) !== 0) { + continue; + } + + const numYellowSockets = item!.numSocketsOfColor(GemColor.GemColorYellow); + + if (numYellowSockets === 0) { + continue; + } + + const socketBonusEP = new Stats(item.item.socketBonus).computeEP(epWeights); + const normalizedEP = socketBonusEP / numYellowSockets; + + if (normalizedEP > maxYellowSocketBonusEP) { + tearSlot = slot; + maxYellowSocketBonusEP = normalizedEP; + } + } + + return tearSlot; +} + +const socketTear = (sim: Sim, gear: Gear, tearSlot: ItemSlot | null, tearColor: GemColor): Gear => { + if (!tearSlot) return gear + + const tearSlotItem = gear.getEquippedItem(tearSlot); + + for (const [socketIdx, socketColor] of tearSlotItem!.allSocketColors().entries()) { + if (socketColor === tearColor) { + return gear.withEquippedItem(tearSlot, tearSlotItem!.withGem(sim.db.lookupGem(49110), socketIdx), true); + } + } + + return gear; +} + +const findSocketsByColor = (player: Player, gear: Gear, epWeights: Stats, color: GemColor, tearSlot: ItemSlot | null): Array<[ItemSlot, number]> => { + const socketList = new Array<[ItemSlot, number]>(); + const isBlacksmithing = player.isBlacksmithing(); + + for (var slot of gear.getItemSlots()) { + const item = gear.getEquippedItem(slot); + + if (!item) { + continue; + } + + const ignoreYellowSockets = ((item!.numSocketsOfColor(GemColor.GemColorBlue) > 0) && (slot !== tearSlot)) + + for (const [socketIdx, socketColor] of item!.curSocketColors(isBlacksmithing).entries()) { + if (item!.hasSocketedGem(socketIdx)) { + continue; + } + + let matchYellowSocket = false; + + if ((socketColor === GemColor.GemColorYellow) && !ignoreYellowSockets) { + matchYellowSocket = new Stats(item.item.socketBonus).computeEP(epWeights) > 1e-8; + } + + if (((color === GemColor.GemColorYellow) && matchYellowSocket) || ((color === GemColor.GemColorRed) && !matchYellowSocket)) { + socketList.push([slot, socketIdx]); + } + } + } + + return socketList; +} + +/** + * Determine if player is trying to reach hard cap + * @remarks + * Used for Feral sim only + */ +const detectArpStackConfiguration = (player: Player, arpCap: number, arpTarget: number): boolean => { + const currentArp = Stats.fromProto(player.getCurrentStats().finalStats).getStat(Stat.StatArmorPenetration); + return (arpTarget > 1000) && (currentArp > 648) && ((currentArp + 20) < arpCap); +} + +const sortYellowSockets = (gear: Gear, yellowSocketList: Array<[ItemSlot, number]>, epWeights: Stats, tearSlot: ItemSlot | null) => { + return yellowSocketList.sort(([slot1], [slot2]) => { + + // If both yellow sockets belong to the same item, then treat them equally. + if (slot1 === slot2) { + return 0; + } + + // If an item already has a Nightmare Tear socketed, then bump up any yellow sockets in it to highest priority. + if (slot1 === tearSlot) { + return -1; + } + + if (slot2 === tearSlot) { + return 1; + } + + // For all other cases, sort by the ratio of the socket bonus value divided by the number of yellow sockets required to activate it. + const item1 = gear.getEquippedItem(slot1); + const bonus1 = new Stats(item1?.item.socketBonus).computeEP(epWeights); + const item2 = gear.getEquippedItem(slot2); + const bonus2 = new Stats(item2?.item.socketBonus).computeEP(epWeights); + return bonus2 / (item2?.numSocketsOfColor(GemColor.GemColorYellow) || 0) - bonus1 / (item1?.numSocketsOfColor(GemColor.GemColorYellow) || 0); + }).reverse(); +} + +export const optimizeGems = async (sim: Sim, player: Player) => { + // First, clear all existing gems + let optimizedGear = player.getGear().withoutGems(); + + // Next, socket the meta + switch (player.spec) { + // Same for all specs + case Spec.SpecFeralDruid: + case Spec.SpecHunter: + case Spec.SpecWarrior: + optimizedGear = optimizedGear.withMetaGem(sim.db.lookupGem(41398)); + } + + // Next, socket a Nightmare Tear in the best socket + const epWeights = player.getEpWeights(); + let tearColor = GemColor.GemColorBlue; + let tearSlot = findBlueTearSlot(optimizedGear, epWeights); + if (tearSlot === null) { + tearColor = GemColor.GemColorYellow; + tearSlot = findYellowTearSlot(optimizedGear, epWeights); + } + optimizedGear = socketTear(sim, optimizedGear, tearSlot, tearColor); + await updateGear(sim, player, optimizedGear); + + let arpTarget = calcArpTarget(optimizedGear).getStat(Stat.StatArmorPenetration); + const arpCap = calcArpCap(arpTarget, player); + const expCap = calcExpCap(player); + const critCap = calcCritCap(optimizedGear); + const hitCap = calcHitCap(player); + + // Should we gem expertise? + const enableExpertiseGemming = player.spec === Spec.SpecFeralDruid || (player.spec === Spec.SpecWarrior && !player.getDisableExpertiseGemming()) + + // Next, identify all sockets where red gems will be placed + const redSockets = findSocketsByColor(player, optimizedGear, epWeights, GemColor.GemColorRed, tearSlot); + // Rank order red gems to use with their associated stat caps + const redGemCaps = new Array<[number, Stats]>(); + redGemCaps.push([GemsByStats.Arp, arpCap]); + + if (enableExpertiseGemming) { + redGemCaps.push([GemsByStats.Exp, expCap]); + } + + // If Feral swap to Agi if below crit cap + if (player.spec === Spec.SpecFeralDruid) { + redGemCaps.push([GemsByStats.Agi, critCap]); + } + // If Feral or Warrior swap to Str when ArP and Crit capped + if (player.spec === Spec.SpecFeralDruid || player.spec === Spec.SpecWarrior) { + redGemCaps.push([GemsByStats.Str, new Stats()]); + } + // If Hunter swap to Agi when ArP and Crit capped + if (player.spec === Spec.SpecHunter) { + redGemCaps.push([GemsByStats.Agi, new Stats()]); + } + + // If JC, then socket 34 ArP gems in first three red sockets before proceeding + let startIdx = 0; + if (player.hasProfession(Profession.Jewelcrafting)) { + optimizedGear = optimizeJewelCraftingGems(sim, player, optimizedGear, redSockets, arpCap, arpTarget); + startIdx = 3; + } + + // Do multiple passes to fill in red gems up their caps + optimizedGear = await fillGemsToCaps(sim, player, optimizedGear, redSockets, redGemCaps, 0, startIdx); + + // Now repeat the process for yellow gems + const yellowSockets = findSocketsByColor(player, optimizedGear, epWeights, GemColor.GemColorYellow, tearSlot); + const yellowGemCaps = new Array<[number, Stats]>(); + + // Rigid Ametrine + yellowGemCaps.push([GemsByStats.Hit, hitCap]); + + switch (player.spec) { + case Spec.SpecFeralDruid: + yellowGemCaps.push([GemsByStats.Exp_Hit, hitCap.add(expCap)]); + + // Allow for socketing ArP gems in weaker yellow sockets after capping Hit and Expertise + // when ArP stacking is detected + if (detectArpStackConfiguration(player, arpCap.getStat(Stat.StatArmorPenetration), arpTarget)) { + sortYellowSockets(optimizedGear, yellowSockets, epWeights, tearSlot); + yellowGemCaps.push([GemsByStats.Arp, arpCap]); + } + + yellowGemCaps.push([GemsByStats.Agi_Hit, hitCap.add(critCap)]); + yellowGemCaps.push([GemsByStats.Str_Hit, hitCap]); + yellowGemCaps.push([GemsByStats.Agi_Crit, critCap]); + yellowGemCaps.push([GemsByStats.Str_Crit, critCap]); + yellowGemCaps.push([GemsByStats.Str_Haste, new Stats()]); + break + case Spec.SpecWarrior: + if (enableExpertiseGemming) yellowGemCaps.push([GemsByStats.Exp_Hit, hitCap.add(expCap)]); + + // Allow for socketing ArP gems in weaker yellow sockets after capping Hit and Expertise + // when ArP stacking is detected + if (detectArpStackConfiguration(player, arpCap.getStat(Stat.StatArmorPenetration), arpTarget)) { + sortYellowSockets(optimizedGear, yellowSockets, epWeights, tearSlot); + yellowGemCaps.push([GemsByStats.Arp, arpCap]); + } + + yellowGemCaps.push([GemsByStats.Str_Hit, hitCap]); + yellowGemCaps.push([GemsByStats.Str_Crit, critCap]); + break + case Spec.SpecHunter: + yellowGemCaps.push([GemsByStats.Agi_Hit, hitCap]); + yellowGemCaps.push([GemsByStats.Agi_Crit, critCap]); + break + } + + + await fillGemsToCaps(sim, player, optimizedGear, yellowSockets, yellowGemCaps, 0, 0); +} + diff --git a/ui/warrior/sim.ts b/ui/warrior/sim.ts index 04036061bd..592e67829d 100644 --- a/ui/warrior/sim.ts +++ b/ui/warrior/sim.ts @@ -8,11 +8,6 @@ import { TristateEffect } from '../core/proto/common.js' import { Stats } from '../core/proto_utils/stats.js'; import { Player } from '../core/player.js'; import { IndividualSimUI } from '../core/individual_sim_ui.js'; -import { TypedEvent } from '../core/typed_event.js'; -import { Gear } from '../core/proto_utils/gear.js'; -import { ItemSlot } from '../core/proto/common.js'; -import { GemColor } from '../core/proto/common.js'; -import { Profession } from '../core/proto/common.js'; import * as OtherInputs from '../core/components/other_inputs.js'; @@ -20,6 +15,7 @@ import * as Mechanics from '../core/constants/mechanics.js'; import * as WarriorInputs from './inputs.js'; import * as Presets from './presets.js'; +import { optimizeGems } from '../shared/auto_gem.js'; export class WarriorSimUI extends IndividualSimUI { constructor(parentElem: HTMLElement, player: Player) { @@ -188,292 +184,7 @@ export class WarriorSimUI extends IndividualSimUI { } addOptimizeGemsAction() { this.addAction('Suggest Gems', 'optimize-gems-action', async () => { - this.optimizeGems(); + optimizeGems(this.sim, this.player); }); } - - async optimizeGems() { - // First, clear all existing gems - let optimizedGear = this.player.getGear().withoutGems(); - - // Next, socket the meta - optimizedGear = optimizedGear.withMetaGem(this.sim.db.lookupGem(41398)); - - // Next, socket a Nightmare Tear in the best blue socket bonus - const epWeights = this.player.getEpWeights(); - const tearSlot = this.findTearSlot(optimizedGear, epWeights); - optimizedGear = this.socketTear(optimizedGear, tearSlot); - await this.updateGear(optimizedGear); - - // Next, identify all sockets where red gems will be placed - const redSockets = this.findSocketsByColor(optimizedGear, epWeights, GemColor.GemColorRed, tearSlot); - - // Rank order red gems to use with their associated stat caps - const redGemCaps = new Array<[number, Stats]>(); - redGemCaps.push([40117, this.calcArpCap(optimizedGear)]); - // Should we gem expertise? - const enableExpertiseGemming = !this.player.getDisableExpertiseGemming() - const expCap = this.calcExpCap(); - if(enableExpertiseGemming){ - redGemCaps.push([40118, expCap]); - } - const critCap = this.calcCritCap(optimizedGear); - redGemCaps.push([40111, new Stats()]); - - // If JC, then socket 34 ArP gems in first three red sockets before proceeding - let startIdx = 0; - - if (this.player.hasProfession(Profession.Jewelcrafting)) { - optimizedGear = this.optimizeJcGems(optimizedGear, redSockets); - startIdx = 3; - } - - // Do multiple passes to fill in red gems up their caps - optimizedGear = await this.fillGemsToCaps(optimizedGear, redSockets, redGemCaps, 0, startIdx); - - // Now repeat the process for yellow gems - const yellowSockets = this.findSocketsByColor(optimizedGear, epWeights, GemColor.GemColorYellow, tearSlot); - const yellowGemCaps = new Array<[number, Stats]>(); - const hitCap = new Stats().withStat(Stat.StatMeleeHit, 8. * 32.79 + 4); - yellowGemCaps.push([40125, hitCap]); - if(enableExpertiseGemming){ - yellowGemCaps.push([40162, hitCap.add(expCap)]); - } - yellowGemCaps.push([40143, hitCap]); - yellowGemCaps.push([40142, critCap]); - await this.fillGemsToCaps(optimizedGear, yellowSockets, yellowGemCaps, 0, 0); - } - - calcExpCap(): Stats { - let expCap = 6.5 * 32.79 + 4; - const weaponMastery = this.player.getTalents().weaponMastery; - const hasWeaponMasteryTalent = !!weaponMastery; - - if (hasWeaponMasteryTalent) { - expCap -= - weaponMastery * 4 * Mechanics.EXPERTISE_PER_QUARTER_PERCENT_REDUCTION; - } - - return new Stats().withStat(Stat.StatExpertise, expCap); - } - - calcArpCap(gear: Gear): Stats { - let arpCap = 1404; - - if (gear.hasTrinket(45931)) { - arpCap = 659; - } else if (gear.hasTrinket(40256)) { - arpCap = 798; - } - - return new Stats().withStat(Stat.StatArmorPenetration, arpCap); - } - - calcArpTarget(gear: Gear): number { - if (gear.hasTrinket(45931)) { - return 648; - } - - if (gear.hasTrinket(40256)) { - return 787; - } - - return 1399; - } - - calcCritCap(gear: Gear): Stats { - const baseCritCapPercentage = 77.8; // includes 3% Crit debuff - let agiProcs = 0; - - if (gear.hasRelic(47668)) { - agiProcs += 200; - } - - if (gear.hasRelic(50456)) { - agiProcs += 44*5; - } - - if (gear.hasTrinket(47131) || gear.hasTrinket(47464)) { - agiProcs += 510; - } - - if (gear.hasTrinket(47115) || gear.hasTrinket(47303)) { - agiProcs += 450; - } - - if (gear.hasTrinket(44253) || gear.hasTrinket(42987)) { - agiProcs += 300; - } - - return new Stats().withStat(Stat.StatMeleeCrit, (baseCritCapPercentage - agiProcs*1.1*1.06*1.02/83.33) * 45.91); - } - - async updateGear(gear: Gear): Promise { - this.player.setGear(TypedEvent.nextEventID(), gear); - await this.sim.updateCharacterStats(TypedEvent.nextEventID()); - return Stats.fromProto(this.player.getCurrentStats().finalStats); - } - - findTearSlot(gear: Gear, epWeights: Stats): ItemSlot | null { - let tearSlot: ItemSlot | null = null; - let maxBlueSocketBonusEP: number = 1e-8; - - for (var slot of gear.getItemSlots()) { - const item = gear.getEquippedItem(slot); - - if (!item) { - continue; - } - - if (item!.numSocketsOfColor(GemColor.GemColorBlue) != 1) { - continue; - } - - const socketBonusEP = new Stats(item.item.socketBonus).computeEP(epWeights); - - if (socketBonusEP > maxBlueSocketBonusEP) { - tearSlot = slot; - maxBlueSocketBonusEP = socketBonusEP; - } - } - - return tearSlot; - } - - socketTear(gear: Gear, tearSlot: ItemSlot | null): Gear { - if (tearSlot != null) { - const tearSlotItem = gear.getEquippedItem(tearSlot); - - for (const [socketIdx, socketColor] of tearSlotItem!.allSocketColors().entries()) { - if (socketColor == GemColor.GemColorBlue) { - return gear.withEquippedItem(tearSlot, tearSlotItem!.withGem(this.sim.db.lookupGem(49110), socketIdx), true); - } - } - } - - return gear; - } - - findSocketsByColor(gear: Gear, epWeights: Stats, color: GemColor, tearSlot: ItemSlot | null): Array<[ItemSlot, number]> { - const socketList = new Array<[ItemSlot, number]>(); - const isBlacksmithing = this.player.isBlacksmithing(); - - for (var slot of gear.getItemSlots()) { - const item = gear.getEquippedItem(slot); - - if (!item) { - continue; - } - - const ignoreYellowSockets = ((item!.numSocketsOfColor(GemColor.GemColorBlue) > 0) && (slot != tearSlot)) - - for (const [socketIdx, socketColor] of item!.curSocketColors(isBlacksmithing).entries()) { - if (item!.hasSocketedGem(socketIdx)) { - continue; - } - - let matchYellowSocket = false; - - if ((socketColor == GemColor.GemColorYellow) && !ignoreYellowSockets) { - matchYellowSocket = new Stats(item.item.socketBonus).computeEP(epWeights) > 1e-8; - } - - if (((color == GemColor.GemColorYellow) && matchYellowSocket) || ((color == GemColor.GemColorRed) && !matchYellowSocket)) { - socketList.push([slot, socketIdx]); - } - } - } - - return socketList; - } - - async fillGemsToCaps(gear: Gear, socketList: Array<[ItemSlot, number]>, gemCaps: Array<[number, Stats]>, numPasses: number, firstIdx: number): Promise { - let updatedGear: Gear = gear; - const currentGem = this.sim.db.lookupGem(gemCaps[numPasses][0]); - - // On the first pass, we simply fill all sockets with the highest priority gem - if (numPasses == 0) { - for (const [itemSlot, socketIdx] of socketList.slice(firstIdx)) { - updatedGear = updatedGear.withGem(itemSlot, socketIdx, currentGem); - } - } - - // If we are below the relevant stat cap for the gem we just filled on the last pass, then we are finished. - let newStats = await this.updateGear(updatedGear); - const currentCap = gemCaps[numPasses][1]; - - if (newStats.belowCaps(currentCap) || (numPasses == gemCaps.length - 1)) { - return updatedGear; - } - - // If we exceeded the stat cap, then work backwards through the socket list and replace each gem with the next highest priority option until we are below the cap - const nextGem = this.sim.db.lookupGem(gemCaps[numPasses + 1][0]); - const nextCap = gemCaps[numPasses + 1][1]; - let capForReplacement = currentCap; - - if ((numPasses > 0) && !currentCap.equals(nextCap)) { - capForReplacement = currentCap.subtract(nextCap); - } - - for (var idx = socketList.length - 1; idx >= firstIdx; idx--) { - if (newStats.belowCaps(capForReplacement)) { - break; - } - - const [itemSlot, socketIdx] = socketList[idx]; - updatedGear = updatedGear.withGem(itemSlot, socketIdx, nextGem); - newStats = await this.updateGear(updatedGear); - } - - // Now run a new pass to check whether we've exceeded the next stat cap - let nextIdx = idx + 1; - - if (!newStats.belowCaps(currentCap)) { - nextIdx = firstIdx; - } - - return await this.fillGemsToCaps(updatedGear, socketList, gemCaps, numPasses + 1, nextIdx); - } - - calcDistanceToArpTarget(numJcArpGems: number, passiveArp: number, numRedSockets: number, arpCap: number, arpTarget: number): number { - const numNormalArpGems = Math.max(0, Math.min(numRedSockets - 3, Math.floor((arpCap - passiveArp - 34 * numJcArpGems) / 20))); - const projectedArp = passiveArp + 34 * numJcArpGems + 20 * numNormalArpGems; - return Math.abs(projectedArp - arpTarget); - } - - optimizeJcGems(gear: Gear, redSocketList: Array<[ItemSlot, number]>): Gear { - const passiveStats = Stats.fromProto(this.player.getCurrentStats().finalStats); - const passiveArp = passiveStats.getStat(Stat.StatArmorPenetration); - const numRedSockets = redSocketList.length; - const arpCap = this.calcArpCap(gear).getStat(Stat.StatArmorPenetration); - const arpTarget = this.calcArpTarget(gear); - - // First determine how many of the JC gems should be 34 ArP gems - let optimalJcArpGems = 0; - let minDistanceToArpTarget = this.calcDistanceToArpTarget(0, passiveArp, numRedSockets, arpCap, arpTarget); - - for (let i = 1; i <= 3; i++) { - const distanceToArpTarget = this.calcDistanceToArpTarget(i, passiveArp, numRedSockets, arpCap, arpTarget); - - if (distanceToArpTarget < minDistanceToArpTarget) { - optimalJcArpGems = i; - minDistanceToArpTarget = distanceToArpTarget; - } - } - - // Now actually socket the gems - let updatedGear: Gear = gear; - - for (let i = 0; i < 3; i++) { - let gemId = 42142; // Str by default - - if (i < optimalJcArpGems) { - gemId = 42153; - } - - updatedGear = updatedGear.withGem(redSocketList[i][0], redSocketList[i][1], this.sim.db.lookupGem(gemId)); - } - - return updatedGear; - } } From 659c74f575e9d85b0bf1984757b816a3239193d5 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Sat, 30 Sep 2023 22:26:20 +0200 Subject: [PATCH 02/13] Add expertise gemming in yellow sockets for warriors --- ui/shared/auto_gem.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index 938883df79..65548ee729 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -254,7 +254,6 @@ const fillGemsToCaps = async (sim: Sim, player: Player, gear: Gear, if (!newStats.belowCaps(currentCap)) { nextIdx = firstIdx; } - return await fillGemsToCaps(sim, player, updatedGear, socketList, gemCaps, numPasses + 1, nextIdx); } @@ -263,6 +262,7 @@ const updateGear = async (sim: Sim, player: Player, gear: Gear): Pr await sim.updateCharacterStats(TypedEvent.nextEventID()); return Stats.fromProto(player.getCurrentStats().finalStats); } + const findBlueTearSlot = (gear: Gear, epWeights: Stats): ItemSlot | null => { let tearSlot: ItemSlot | null = null; let maxBlueSocketBonusEP: number = 1e-8; @@ -443,7 +443,6 @@ export const optimizeGems = async (sim: Sim, player: Player) => { // Rank order red gems to use with their associated stat caps const redGemCaps = new Array<[number, Stats]>(); redGemCaps.push([GemsByStats.Arp, arpCap]); - if (enableExpertiseGemming) { redGemCaps.push([GemsByStats.Exp, expCap]); } @@ -496,7 +495,10 @@ export const optimizeGems = async (sim: Sim, player: Player) => { yellowGemCaps.push([GemsByStats.Str_Haste, new Stats()]); break case Spec.SpecWarrior: - if (enableExpertiseGemming) yellowGemCaps.push([GemsByStats.Exp_Hit, hitCap.add(expCap)]); + if (enableExpertiseGemming) { + yellowGemCaps.push([GemsByStats.Exp_Hit, hitCap.add(expCap)]); + yellowGemCaps.push([GemsByStats.Exp, expCap]); + } // Allow for socketing ArP gems in weaker yellow sockets after capping Hit and Expertise // when ArP stacking is detected From 3f08a726ef14f864cd39c3efcf1681d5b9eeabe8 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Sun, 1 Oct 2023 18:33:41 +0200 Subject: [PATCH 03/13] Fix broken expertise gemming check --- ui/shared/auto_gem.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index 65548ee729..15f7d615d9 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -5,6 +5,7 @@ import { Stats } from "../core/proto_utils/stats"; import { Sim } from "../core/sim"; import { TypedEvent } from "../core/typed_event"; import * as Mechanics from '../core/constants/mechanics.js'; +import { Warrior_Options as WarriorOptions } from "ui/core/proto/warrior"; /*** * WARNING: Currently only optimised for Arp/Exp/Hit gemming the following specs; @@ -436,7 +437,7 @@ export const optimizeGems = async (sim: Sim, player: Player) => { const hitCap = calcHitCap(player); // Should we gem expertise? - const enableExpertiseGemming = player.spec === Spec.SpecFeralDruid || (player.spec === Spec.SpecWarrior && !player.getDisableExpertiseGemming()) + const enableExpertiseGemming = player.spec === Spec.SpecFeralDruid || (player.spec === Spec.SpecWarrior && !((player.getSpecOptions() as WarriorOptions).disableExpertiseGemming)) // Next, identify all sockets where red gems will be placed const redSockets = findSocketsByColor(player, optimizedGear, epWeights, GemColor.GemColorRed, tearSlot); From def4d1ae759bec41be43142b080e09bd4ebb9b72 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Mon, 2 Oct 2023 08:52:57 +0200 Subject: [PATCH 04/13] Fix comment --- ui/shared/auto_gem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index 15f7d615d9..b4d1e69567 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -456,7 +456,7 @@ export const optimizeGems = async (sim: Sim, player: Player) => { if (player.spec === Spec.SpecFeralDruid || player.spec === Spec.SpecWarrior) { redGemCaps.push([GemsByStats.Str, new Stats()]); } - // If Hunter swap to Agi when ArP and Crit capped + // If Hunter swap to Agi when ArP capped if (player.spec === Spec.SpecHunter) { redGemCaps.push([GemsByStats.Agi, new Stats()]); } From d7677624378e7d061cb521421ee19372cfa01792 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Mon, 2 Oct 2023 09:09:46 +0200 Subject: [PATCH 05/13] Add hunter yellow socket arp capping --- ui/shared/auto_gem.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index b4d1e69567..4cc8bf42b5 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -513,6 +513,14 @@ export const optimizeGems = async (sim: Sim, player: Player) => { break case Spec.SpecHunter: yellowGemCaps.push([GemsByStats.Agi_Hit, hitCap]); + + // Allow for socketing ArP gems in weaker yellow sockets after capping Hit + // when ArP stacking is detected + if (detectArpStackConfiguration(player, arpCap.getStat(Stat.StatArmorPenetration), arpTarget)) { + sortYellowSockets(optimizedGear, yellowSockets, epWeights, tearSlot); + yellowGemCaps.push([GemsByStats.Arp, arpCap]); + } + yellowGemCaps.push([GemsByStats.Agi_Crit, critCap]); break } From 9590e3594fc6c4382cd1be5bdf9c7292bff823ef Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Wed, 4 Oct 2023 22:22:50 +0200 Subject: [PATCH 06/13] Fix detect arp feral only comment --- ui/shared/auto_gem.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index 4cc8bf42b5..8f6d2f5660 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -372,8 +372,6 @@ const findSocketsByColor = (player: Player, gear: Gear, epWeights: /** * Determine if player is trying to reach hard cap - * @remarks - * Used for Feral sim only */ const detectArpStackConfiguration = (player: Player, arpCap: number, arpTarget: number): boolean => { const currentArp = Stats.fromProto(player.getCurrentStats().finalStats).getStat(Stat.StatArmorPenetration); From 4a889ab38da28a2208d54caf1232c85e080cf040 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Wed, 4 Oct 2023 22:56:06 +0200 Subject: [PATCH 07/13] Add break to juicer gem slot selection --- ui/shared/auto_gem.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index 8f6d2f5660..0734af1f0c 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -181,10 +181,13 @@ const optimizeJewelCraftingGems = (sim: Sim, player: Player, gear: switch (player.spec) { case Spec.SpecHunter: gemId = 42143 // Agi by default + break; case Spec.SpecWarrior: gemId = 42142 // Str by default + break; case Spec.SpecFeralDruid: gemId = 42142 // Str by default + break; } From 185570207b8fb1349520f3358ae463f1ff1f55e8 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Wed, 4 Oct 2023 23:17:05 +0200 Subject: [PATCH 08/13] Add dungeon arp trinket & fix arp stacking only as mm --- ui/shared/auto_gem.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index 0734af1f0c..c0860b939d 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -52,6 +52,11 @@ const calcArpCap = (arpTarget: number, player: Player): Stats => { const calcArpTarget = (gear: Gear): Stats => { let arpCap = 1399; + // Needle-Encrusted Scorpion + if (gear.hasTrinket(50198)) { + arpCap -= 678; + } + // Mjolnir Runestone if (gear.hasTrinket(45931)) { arpCap -= 751; @@ -444,7 +449,15 @@ export const optimizeGems = async (sim: Sim, player: Player) => { const redSockets = findSocketsByColor(player, optimizedGear, epWeights, GemColor.GemColorRed, tearSlot); // Rank order red gems to use with their associated stat caps const redGemCaps = new Array<[number, Stats]>(); - redGemCaps.push([GemsByStats.Arp, arpCap]); + + if ( + player.spec === Spec.SpecFeralDruid || + player.spec === Spec.SpecWarrior || + (Spec.SpecHunter && player.getTalentTree() === 1) + ) { + redGemCaps.push([GemsByStats.Arp, arpCap]); + } + if (enableExpertiseGemming) { redGemCaps.push([GemsByStats.Exp, expCap]); } @@ -516,8 +529,8 @@ export const optimizeGems = async (sim: Sim, player: Player) => { yellowGemCaps.push([GemsByStats.Agi_Hit, hitCap]); // Allow for socketing ArP gems in weaker yellow sockets after capping Hit - // when ArP stacking is detected - if (detectArpStackConfiguration(player, arpCap.getStat(Stat.StatArmorPenetration), arpTarget)) { + // when ArP stacking is detected and spec is Marksman + if (player.getTalentTree() === 1 && detectArpStackConfiguration(player, arpCap.getStat(Stat.StatArmorPenetration), arpTarget)) { sortYellowSockets(optimizedGear, yellowSockets, epWeights, tearSlot); yellowGemCaps.push([GemsByStats.Arp, arpCap]); } From 3d786fbc00465a04370886695268e9c88a220929 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Wed, 4 Oct 2023 23:29:46 +0200 Subject: [PATCH 09/13] Add MM check to JC gemming --- ui/feral_druid/sim.ts | 2 +- ui/hunter/sim.ts | 2 +- ui/shared/auto_gem.ts | 15 ++++++++++----- ui/warrior/sim.ts | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ui/feral_druid/sim.ts b/ui/feral_druid/sim.ts index 7218566b8d..8c4c83bfe8 100644 --- a/ui/feral_druid/sim.ts +++ b/ui/feral_druid/sim.ts @@ -164,7 +164,7 @@ export class FeralDruidSimUI extends IndividualSimUI { addOptimizeGemsAction() { this.addAction('Suggest Gems', 'optimize-gems-action', async () => { - optimizeGems(this.sim, this.player); + await optimizeGems(this.sim, this.player); }); } } diff --git a/ui/hunter/sim.ts b/ui/hunter/sim.ts index 52b0cb2d0a..cc53ca23f3 100644 --- a/ui/hunter/sim.ts +++ b/ui/hunter/sim.ts @@ -371,7 +371,7 @@ export class HunterSimUI extends IndividualSimUI { } addOptimizeGemsAction() { this.addAction('Suggest Gems', 'optimize-gems-action', async () => { - optimizeGems(this.sim, this.player); + await optimizeGems(this.sim, this.player); }); } } diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index c0860b939d..02331dde0a 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -195,11 +195,16 @@ const optimizeJewelCraftingGems = (sim: Sim, player: Player, gear: break; } - - if (i < optimalJcArpGems) { - gemId = 42153; // ArP - } else if ((player.spec === Spec.SpecFeralDruid) && isBelowCritCap) { - gemId = 42143; // Below crit swap to Agi + if ( + Spec.SpecWarrior || + Spec.SpecFeralDruid || + Spec.SpecHunter && player.getTalentTree() === 1 + ) { + if (i < optimalJcArpGems) { + gemId = 42153; // ArP + } else if ((player.spec === Spec.SpecFeralDruid) && isBelowCritCap) { + gemId = 42143; // Below crit swap to Agi + } } if (gemId) updatedGear = updatedGear.withGem(redSocketList[i][0], redSocketList[i][1], sim.db.lookupGem(gemId)); diff --git a/ui/warrior/sim.ts b/ui/warrior/sim.ts index 592e67829d..826a34200d 100644 --- a/ui/warrior/sim.ts +++ b/ui/warrior/sim.ts @@ -184,7 +184,7 @@ export class WarriorSimUI extends IndividualSimUI { } addOptimizeGemsAction() { this.addAction('Suggest Gems', 'optimize-gems-action', async () => { - optimizeGems(this.sim, this.player); + await optimizeGems(this.sim, this.player); }); } } From a83e2fe70932251089eb0b11d3d3168a2bf81a1f Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Wed, 4 Oct 2023 23:38:50 +0200 Subject: [PATCH 10/13] Fix typo in if check --- ui/shared/auto_gem.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index 02331dde0a..b44d0eb082 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -196,9 +196,9 @@ const optimizeJewelCraftingGems = (sim: Sim, player: Player, gear: } if ( - Spec.SpecWarrior || - Spec.SpecFeralDruid || - Spec.SpecHunter && player.getTalentTree() === 1 + player.spec === Spec.SpecWarrior || + player.spec === Spec.SpecFeralDruid || + player.spec === Spec.SpecHunter && player.getTalentTree() === 1 ) { if (i < optimalJcArpGems) { gemId = 42153; // ArP From e38eb4627f5e4dc729cd3bebd8627d0b4b4599f4 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Thu, 5 Oct 2023 16:14:46 +0200 Subject: [PATCH 11/13] Fix broken tear socketing --- ui/shared/auto_gem.ts | 73 ++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index b44d0eb082..0e70e47b38 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -6,6 +6,7 @@ import { Sim } from "../core/sim"; import { TypedEvent } from "../core/typed_event"; import * as Mechanics from '../core/constants/mechanics.js'; import { Warrior_Options as WarriorOptions } from "ui/core/proto/warrior"; +import { EquippedItem } from "ui/core/proto_utils/equipped_item"; /*** * WARNING: Currently only optimised for Arp/Exp/Hit gemming the following specs; @@ -158,10 +159,31 @@ const calcHitCap = (player: Player): Stats => { return new Stats().withStat(Stat.StatMeleeHit, hitCap) } + +/** + * Determine if player is trying to reach hard cap + */ +const detectArpStackConfiguration = (player: Player, arpCap: number, arpTarget: number): boolean => { + const currentArp = getArpValue(Stats.fromProto(player.getCurrentStats().finalStats)); + return (arpTarget > 1000) && (currentArp > 648) && ((currentArp + 20) < arpCap); +} + +/** + * Get ArP as a number + */ +const getArpValue = (stats: Stats) => stats.getStat(Stat.StatArmorPenetration) + +/** + * Get the items socket bonus EP value + */ +const getItemSocketsEpValue = (item: EquippedItem | null, weights: Stats) => new Stats(item?.item.socketBonus).computeEP(weights) + +const isMarksmanship = (player: Player) => player.spec === Spec.SpecHunter && player.getTalentTree() === 1 + const optimizeJewelCraftingGems = (sim: Sim, player: Player, gear: Gear, redSocketList: [ItemSlot, number][], arpCap: Stats, arpTarget: number): Gear => { const passiveStats = Stats.fromProto(player.getCurrentStats().finalStats); - const passiveArp = passiveStats.getStat(Stat.StatArmorPenetration); - const arpCapValue = arpCap.getStat(Stat.StatArmorPenetration); + const passiveArp = getArpValue(passiveStats); + const arpCapValue = getArpValue(arpCap); const numRedSockets = redSocketList.length; const isBelowCritCap = passiveStats.belowCaps(calcCritCap(gear)); @@ -198,7 +220,7 @@ const optimizeJewelCraftingGems = (sim: Sim, player: Player, gear: if ( player.spec === Spec.SpecWarrior || player.spec === Spec.SpecFeralDruid || - player.spec === Spec.SpecHunter && player.getTalentTree() === 1 + isMarksmanship(player) ) { if (i < optimalJcArpGems) { gemId = 42153; // ArP @@ -252,7 +274,8 @@ const fillGemsToCaps = async (sim: Sim, player: Player, gear: Gear, break } - for (var idx = socketList.length - 1; idx >= firstIdx; idx--) { + let idx = socketList.length - 1; + for (; idx >= firstIdx; idx--) { if (newStats.belowCaps(capForReplacement)) { break; } @@ -281,9 +304,8 @@ const findBlueTearSlot = (gear: Gear, epWeights: Stats): ItemSlot | null => { let tearSlot: ItemSlot | null = null; let maxBlueSocketBonusEP: number = 1e-8; - for (var slot of gear.getItemSlots()) { + for (let slot of gear.getItemSlots()) { const item = gear.getEquippedItem(slot); - if (!item) { continue; } @@ -307,7 +329,7 @@ const findYellowTearSlot = (gear: Gear, epWeights: Stats): ItemSlot | null => { let tearSlot: ItemSlot | null = null; let maxYellowSocketBonusEP: number = 1e-8; - for (var slot of gear.getItemSlots()) { + for (let slot of gear.getItemSlots()) { const item = gear.getEquippedItem(slot); if (!item) { @@ -337,10 +359,9 @@ const findYellowTearSlot = (gear: Gear, epWeights: Stats): ItemSlot | null => { } const socketTear = (sim: Sim, gear: Gear, tearSlot: ItemSlot | null, tearColor: GemColor): Gear => { - if (!tearSlot) return gear + if (tearSlot === null) return gear const tearSlotItem = gear.getEquippedItem(tearSlot); - for (const [socketIdx, socketColor] of tearSlotItem!.allSocketColors().entries()) { if (socketColor === tearColor) { return gear.withEquippedItem(tearSlot, tearSlotItem!.withGem(sim.db.lookupGem(49110), socketIdx), true); @@ -354,7 +375,7 @@ const findSocketsByColor = (player: Player, gear: Gear, epWeights: const socketList = new Array<[ItemSlot, number]>(); const isBlacksmithing = player.isBlacksmithing(); - for (var slot of gear.getItemSlots()) { + for (let slot of gear.getItemSlots()) { const item = gear.getEquippedItem(slot); if (!item) { @@ -383,14 +404,6 @@ const findSocketsByColor = (player: Player, gear: Gear, epWeights: return socketList; } -/** - * Determine if player is trying to reach hard cap - */ -const detectArpStackConfiguration = (player: Player, arpCap: number, arpTarget: number): boolean => { - const currentArp = Stats.fromProto(player.getCurrentStats().finalStats).getStat(Stat.StatArmorPenetration); - return (arpTarget > 1000) && (currentArp > 648) && ((currentArp + 20) < arpCap); -} - const sortYellowSockets = (gear: Gear, yellowSocketList: Array<[ItemSlot, number]>, epWeights: Stats, tearSlot: ItemSlot | null) => { return yellowSocketList.sort(([slot1], [slot2]) => { @@ -410,9 +423,9 @@ const sortYellowSockets = (gear: Gear, yellowSocketList: Array<[ItemSlot, number // For all other cases, sort by the ratio of the socket bonus value divided by the number of yellow sockets required to activate it. const item1 = gear.getEquippedItem(slot1); - const bonus1 = new Stats(item1?.item.socketBonus).computeEP(epWeights); + const bonus1 = getItemSocketsEpValue(item1, epWeights); const item2 = gear.getEquippedItem(slot2); - const bonus2 = new Stats(item2?.item.socketBonus).computeEP(epWeights); + const bonus2 = getItemSocketsEpValue(item2, epWeights); return bonus2 / (item2?.numSocketsOfColor(GemColor.GemColorYellow) || 0) - bonus1 / (item1?.numSocketsOfColor(GemColor.GemColorYellow) || 0); }).reverse(); } @@ -434,14 +447,16 @@ export const optimizeGems = async (sim: Sim, player: Player) => { const epWeights = player.getEpWeights(); let tearColor = GemColor.GemColorBlue; let tearSlot = findBlueTearSlot(optimizedGear, epWeights); + if (tearSlot === null) { tearColor = GemColor.GemColorYellow; tearSlot = findYellowTearSlot(optimizedGear, epWeights); } + optimizedGear = socketTear(sim, optimizedGear, tearSlot, tearColor); await updateGear(sim, player, optimizedGear); - let arpTarget = calcArpTarget(optimizedGear).getStat(Stat.StatArmorPenetration); + let arpTarget = getArpValue(calcArpTarget(optimizedGear)); const arpCap = calcArpCap(arpTarget, player); const expCap = calcExpCap(player); const critCap = calcCritCap(optimizedGear); @@ -458,7 +473,7 @@ export const optimizeGems = async (sim: Sim, player: Player) => { if ( player.spec === Spec.SpecFeralDruid || player.spec === Spec.SpecWarrior || - (Spec.SpecHunter && player.getTalentTree() === 1) + isMarksmanship(player) && detectArpStackConfiguration(player, getArpValue(arpCap), arpTarget) ) { redGemCaps.push([GemsByStats.Arp, arpCap]); } @@ -503,7 +518,7 @@ export const optimizeGems = async (sim: Sim, player: Player) => { // Allow for socketing ArP gems in weaker yellow sockets after capping Hit and Expertise // when ArP stacking is detected - if (detectArpStackConfiguration(player, arpCap.getStat(Stat.StatArmorPenetration), arpTarget)) { + if (detectArpStackConfiguration(player, getArpValue(arpCap), arpTarget)) { sortYellowSockets(optimizedGear, yellowSockets, epWeights, tearSlot); yellowGemCaps.push([GemsByStats.Arp, arpCap]); } @@ -513,7 +528,7 @@ export const optimizeGems = async (sim: Sim, player: Player) => { yellowGemCaps.push([GemsByStats.Agi_Crit, critCap]); yellowGemCaps.push([GemsByStats.Str_Crit, critCap]); yellowGemCaps.push([GemsByStats.Str_Haste, new Stats()]); - break + break; case Spec.SpecWarrior: if (enableExpertiseGemming) { yellowGemCaps.push([GemsByStats.Exp_Hit, hitCap.add(expCap)]); @@ -522,26 +537,26 @@ export const optimizeGems = async (sim: Sim, player: Player) => { // Allow for socketing ArP gems in weaker yellow sockets after capping Hit and Expertise // when ArP stacking is detected - if (detectArpStackConfiguration(player, arpCap.getStat(Stat.StatArmorPenetration), arpTarget)) { + if (detectArpStackConfiguration(player, getArpValue(arpCap), arpTarget)) { sortYellowSockets(optimizedGear, yellowSockets, epWeights, tearSlot); yellowGemCaps.push([GemsByStats.Arp, arpCap]); } yellowGemCaps.push([GemsByStats.Str_Hit, hitCap]); yellowGemCaps.push([GemsByStats.Str_Crit, critCap]); - break + break; case Spec.SpecHunter: yellowGemCaps.push([GemsByStats.Agi_Hit, hitCap]); // Allow for socketing ArP gems in weaker yellow sockets after capping Hit - // when ArP stacking is detected and spec is Marksman - if (player.getTalentTree() === 1 && detectArpStackConfiguration(player, arpCap.getStat(Stat.StatArmorPenetration), arpTarget)) { + // when spec is Marksmanship and ArP stacking is detected + if (isMarksmanship(player) && detectArpStackConfiguration(player, getArpValue(arpCap), arpTarget)) { sortYellowSockets(optimizedGear, yellowSockets, epWeights, tearSlot); yellowGemCaps.push([GemsByStats.Arp, arpCap]); } yellowGemCaps.push([GemsByStats.Agi_Crit, critCap]); - break + break; } From bc606421c6d8a4c8b6cc9f30470e60136950a04d Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Thu, 5 Oct 2023 19:18:30 +0200 Subject: [PATCH 12/13] Fix MM check for JC ArP --- ui/shared/auto_gem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index 0e70e47b38..555bb46585 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -220,7 +220,7 @@ const optimizeJewelCraftingGems = (sim: Sim, player: Player, gear: if ( player.spec === Spec.SpecWarrior || player.spec === Spec.SpecFeralDruid || - isMarksmanship(player) + isMarksmanship(player) && detectArpStackConfiguration(player, getArpValue(arpCap), arpTarget) ) { if (i < optimalJcArpGems) { gemId = 42153; // ArP From 49a419d8896ae9744b7ec923d699d27942c85d60 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Fri, 6 Oct 2023 17:18:12 +0200 Subject: [PATCH 13/13] Add additional tear socket bonus selection check --- ui/shared/auto_gem.ts | 68 ++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/ui/shared/auto_gem.ts b/ui/shared/auto_gem.ts index 555bb46585..d684c58bc9 100644 --- a/ui/shared/auto_gem.ts +++ b/ui/shared/auto_gem.ts @@ -163,7 +163,7 @@ const calcHitCap = (player: Player): Stats => { /** * Determine if player is trying to reach hard cap */ -const detectArpStackConfiguration = (player: Player, arpCap: number, arpTarget: number): boolean => { +const detectArpStackConfiguration = (player: Player, arpCap: number, arpTarget: number): boolean => { const currentArp = getArpValue(Stats.fromProto(player.getCurrentStats().finalStats)); return (arpTarget > 1000) && (currentArp > 648) && ((currentArp + 20) < arpCap); } @@ -180,6 +180,12 @@ const getItemSocketsEpValue = (item: EquippedItem | null, weights: Stats) => new const isMarksmanship = (player: Player) => player.spec === Spec.SpecHunter && player.getTalentTree() === 1 +const isEligibleForArpStacking = (player: Player, arpCap: number, arpTarget: number) => { + return player.spec === Spec.SpecWarrior || + player.spec === Spec.SpecFeralDruid || + isMarksmanship(player) && detectArpStackConfiguration(player, arpCap, arpTarget) +} + const optimizeJewelCraftingGems = (sim: Sim, player: Player, gear: Gear, redSocketList: [ItemSlot, number][], arpCap: Stats, arpTarget: number): Gear => { const passiveStats = Stats.fromProto(player.getCurrentStats().finalStats); const passiveArp = getArpValue(passiveStats); @@ -217,11 +223,7 @@ const optimizeJewelCraftingGems = (sim: Sim, player: Player, gear: break; } - if ( - player.spec === Spec.SpecWarrior || - player.spec === Spec.SpecFeralDruid || - isMarksmanship(player) && detectArpStackConfiguration(player, getArpValue(arpCap), arpTarget) - ) { + if (isEligibleForArpStacking(player, getArpValue(arpCap), arpTarget)) { if (i < optimalJcArpGems) { gemId = 42153; // ArP } else if ((player.spec === Spec.SpecFeralDruid) && isBelowCritCap) { @@ -300,22 +302,23 @@ const updateGear = async (sim: Sim, player: Player, gear: Gear): Pr return Stats.fromProto(player.getCurrentStats().finalStats); } -const findBlueTearSlot = (gear: Gear, epWeights: Stats): ItemSlot | null => { +const findBlueTearSlot = (gear: Gear, epWeights: Stats, player: Player, arpCap: number, arpTarget: number): ItemSlot | null => { let tearSlot: ItemSlot | null = null; let maxBlueSocketBonusEP: number = 1e-8; + const isArpStacking = isEligibleForArpStacking(player, arpCap, arpTarget) for (let slot of gear.getItemSlots()) { const item = gear.getEquippedItem(slot); - if (!item) { - continue; - } - if (item.numSocketsOfColor(GemColor.GemColorBlue) !== 1) { + if ( + !item || + item.numSocketsOfColor(GemColor.GemColorBlue) !== 1 || + (isArpStacking && item.numPossibleSockets === 3 && !!item.numSocketsOfColor(GemColor.GemColorYellow)) + ) { continue; } const socketBonusEP = new Stats(item.item.socketBonus).computeEP(epWeights); - if (socketBonusEP > maxBlueSocketBonusEP) { tearSlot = slot; maxBlueSocketBonusEP = socketBonusEP; @@ -332,16 +335,11 @@ const findYellowTearSlot = (gear: Gear, epWeights: Stats): ItemSlot | null => { for (let slot of gear.getItemSlots()) { const item = gear.getEquippedItem(slot); - if (!item) { - continue; - } - - if (item.numSocketsOfColor(GemColor.GemColorBlue) !== 0) { + if (!item || item.numSocketsOfColor(GemColor.GemColorBlue) !== 0) { continue; } - const numYellowSockets = item!.numSocketsOfColor(GemColor.GemColorYellow); - + const numYellowSockets = item?.numSocketsOfColor(GemColor.GemColorYellow); if (numYellowSockets === 0) { continue; } @@ -431,10 +429,10 @@ const sortYellowSockets = (gear: Gear, yellowSocketList: Array<[ItemSlot, number } export const optimizeGems = async (sim: Sim, player: Player) => { - // First, clear all existing gems + // Clear all gems let optimizedGear = player.getGear().withoutGems(); - // Next, socket the meta + // Socket meta gem switch (player.spec) { // Same for all specs case Spec.SpecFeralDruid: @@ -443,10 +441,19 @@ export const optimizeGems = async (sim: Sim, player: Player) => { optimizedGear = optimizedGear.withMetaGem(sim.db.lookupGem(41398)); } - // Next, socket a Nightmare Tear in the best socket const epWeights = player.getEpWeights(); + const arpTarget = getArpValue(calcArpTarget(optimizedGear)); + const arpCap = calcArpCap(arpTarget, player); + const expCap = calcExpCap(player); + const critCap = calcCritCap(optimizedGear); + const hitCap = calcHitCap(player); + + // Should we gem expertise? + const enableExpertiseGemming = player.spec === Spec.SpecFeralDruid || (player.spec === Spec.SpecWarrior && !((player.getSpecOptions() as WarriorOptions).disableExpertiseGemming)) + + // Next, socket a Nightmare Tear in the best socket (blue, followed by yellow) let tearColor = GemColor.GemColorBlue; - let tearSlot = findBlueTearSlot(optimizedGear, epWeights); + let tearSlot = findBlueTearSlot(optimizedGear, epWeights, player, getArpValue(arpCap), arpTarget); if (tearSlot === null) { tearColor = GemColor.GemColorYellow; @@ -456,25 +463,12 @@ export const optimizeGems = async (sim: Sim, player: Player) => { optimizedGear = socketTear(sim, optimizedGear, tearSlot, tearColor); await updateGear(sim, player, optimizedGear); - let arpTarget = getArpValue(calcArpTarget(optimizedGear)); - const arpCap = calcArpCap(arpTarget, player); - const expCap = calcExpCap(player); - const critCap = calcCritCap(optimizedGear); - const hitCap = calcHitCap(player); - - // Should we gem expertise? - const enableExpertiseGemming = player.spec === Spec.SpecFeralDruid || (player.spec === Spec.SpecWarrior && !((player.getSpecOptions() as WarriorOptions).disableExpertiseGemming)) - // Next, identify all sockets where red gems will be placed const redSockets = findSocketsByColor(player, optimizedGear, epWeights, GemColor.GemColorRed, tearSlot); // Rank order red gems to use with their associated stat caps const redGemCaps = new Array<[number, Stats]>(); - if ( - player.spec === Spec.SpecFeralDruid || - player.spec === Spec.SpecWarrior || - isMarksmanship(player) && detectArpStackConfiguration(player, getArpValue(arpCap), arpTarget) - ) { + if (isEligibleForArpStacking(player, getArpValue(arpCap), arpTarget)) { redGemCaps.push([GemsByStats.Arp, arpCap]); }