Skip to content

Commit

Permalink
Merge pull request #3377 from NerdEgghead/master
Browse files Browse the repository at this point in the history
Added gem optimizer for Feral DPS sim
  • Loading branch information
NerdEgghead authored Jul 27, 2023
2 parents 3aa60cd + b1bf06d commit 8f29f77
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 1 deletion.
4 changes: 4 additions & 0 deletions ui/core/proto_utils/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ export class Database {
return Object.values(this.gems).filter(gem => gemMatchesSocket(gem, socketColor));
}

lookupGem(itemID: number): Gem {
return this.gems[itemID]
}

lookupItemSpec(itemSpec: ItemSpec): EquippedItem | null {
const item = this.items[itemSpec.id];
if (!item)
Expand Down
26 changes: 26 additions & 0 deletions ui/core/proto_utils/equipped_item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ export class EquippedItem {
return curItem;
}

removeAllGems(): EquippedItem {
let curItem: EquippedItem | null = this;

for (let i = 0; i < curItem._gems.length; i++) {
curItem = curItem.withGemHelper(null, i);
}

return curItem;
}

asActionId(): ActionId {
return ActionId.fromItemId(this._item.id);
}
Expand Down Expand Up @@ -203,10 +213,26 @@ export class EquippedItem {
return this._item.gemSockets.length + (this.hasExtraSocket(isBlacksmithing) ? 1 : 0);
}

numSocketsOfColor(color: GemColor): number {
let numSockets: number = 0;

for (var socketColor of this._item.gemSockets) {
if (socketColor == color) {
numSockets += 1;
}
}

return numSockets;
}

hasExtraGem(): boolean {
return this._gems.length > this.item.gemSockets.length;
}

hasSocketedGem(socketIdx: number): boolean {
return this._gems[socketIdx] != null;
}

allSocketColors(): Array<GemColor> {
return this.couldHaveExtraSocket() ? this._item.gemSockets.concat([GemColor.GemColorPrismatic]) : this._item.gemSockets;
}
Expand Down
48 changes: 48 additions & 0 deletions ui/core/proto_utils/gear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ export class Gear extends BaseGear {
return this.getTrinkets().map(t => t?.item.id).includes(itemId);
}

hasRelic(itemId: number): boolean {
const relicItem = this.getEquippedItem(ItemSlot.ItemSlotRanged);

if (!relicItem) {
return false;
}

return relicItem!.item.id == itemId;
}

asMap(): InternalGear {
const newInternalGear: Partial<InternalGear> = {};
getEnumValues(ItemSlot).map(slot => Number(slot) as ItemSlot).forEach(slot => {
Expand Down Expand Up @@ -226,6 +236,30 @@ export class Gear extends BaseGear {
return this.getMetaGem() != null && !this.hasActiveMetaGem(isBlacksmithing);
}

withGem(itemSlot: ItemSlot, socketIdx: number, gem: Gem): Gear {
const item = this.getEquippedItem(itemSlot);

if (item) {
return this.withEquippedItem(itemSlot, item.withGem(gem, socketIdx), true);
}

return this;
}

withMetaGem(metaGem: Gem): Gear {
const headItem = this.getEquippedItem(ItemSlot.ItemSlotHead);

if (headItem) {
for (const [socketIdx, socketColor] of headItem.allSocketColors().entries()) {
if (socketColor == GemColor.GemColorMeta) {
return this.withEquippedItem(ItemSlot.ItemSlotHead, headItem.withGem(metaGem, socketIdx), true);
}
}
}

return this;
}

withoutMetaGem(): Gear {
const headItem = this.getEquippedItem(ItemSlot.ItemSlotHead);
const metaGem = this.getMetaGem();
Expand All @@ -236,6 +270,20 @@ export class Gear extends BaseGear {
}
}

withoutGems(): Gear {
let curGear: Gear = this;

for (var slot of this.getItemSlots()) {
const item = this.getEquippedItem(slot);

if (item) {
curGear = curGear.withEquippedItem(slot, item.removeAllGems(), true);
}
}

return curGear;
}

// Removes bonus gems from blacksmith profession bonus.
withoutBlacksmithSockets(): Gear {
let curGear: Gear = this;
Expand Down
10 changes: 10 additions & 0 deletions ui/core/proto_utils/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@ export class Stats {
return total;
}

belowCaps(statCaps: Stats): boolean {
for (const [idx, stat] of this.stats.entries()) {
if ((statCaps.stats[idx] > 0) && (stat > statCaps.stats[idx])) {
return false;
}
}

return true;
}

asArray(): Array<number> {
return this.stats.slice();
}
Expand Down
2 changes: 1 addition & 1 deletion ui/core/sim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ export class Sim {
}

// This should be invoked internally whenever stats might have changed.
private async updateCharacterStats(eventID: EventID) {
async updateCharacterStats(eventID: EventID) {
if (eventID == 0) {
// Skip the first event ID because it interferes with the loaded stats.
return;
Expand Down
221 changes: 221 additions & 0 deletions ui/feral_druid/sim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ 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';
Expand Down Expand Up @@ -157,5 +162,221 @@ export class FeralDruidSimUI extends IndividualSimUI<Spec.SpecFeralDruid> {
],
},
});

this.addOptimizeGemsAction();
}

addOptimizeGemsAction() {
this.addAction('Suggest Gems', 'optimize-gems-action', async () => {
this.optimizeGems();
});
}

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)]);
const expCap = new Stats().withStat(Stat.StatExpertise, 6.5 * 32.79);
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)) {
for (const [itemSlot, socketIdx] of redSockets.slice(0, 3)) {
optimizedGear = optimizedGear.withGem(itemSlot, socketIdx, this.sim.db.lookupGem(42153));
}

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);
yellowGemCaps.push([40125, hitCap]);
yellowGemCaps.push([40162, hitCap.add(expCap)]);
yellowGemCaps.push([40148, hitCap.add(critCap)]);
yellowGemCaps.push([40147, critCap]);
yellowGemCaps.push([40143, hitCap]);
yellowGemCaps.push([40142, critCap]);
yellowGemCaps.push([40146, new Stats()]);
await this.fillGemsToCaps(optimizedGear, yellowSockets, yellowGemCaps, 0, 0);
}

calcArpCap(gear: Gear): Stats {
let arpCap = 1399;

if (gear.hasTrinket(45931)) {
arpCap = 659;
} else if (gear.hasTrinket(40256)) {
arpCap = 798;
}

return new Stats().withStat(Stat.StatArmorPenetration, arpCap);
}

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) {
this.player.setGear(TypedEvent.nextEventID(), gear);
await this.sim.updateCharacterStats(TypedEvent.nextEventID());
}

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) {
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]>();

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!.allSocketColors().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<Gear> {
let updatedGear: Gear = gear;
let nextGem = 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, nextGem);
}
}

// Update player stats after the last pass
await this.updateGear(updatedGear);

// If we are below the relevant stat cap for the gem we just filled on the last pass, then we are finished.
let newStats = Stats.fromProto(this.player.getCurrentStats().finalStats);

if (newStats.belowCaps(gemCaps[numPasses][1]) || (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
nextGem = this.sim.db.lookupGem(gemCaps[numPasses + 1][0]);

for (var idx = socketList.length - 1; idx >= firstIdx; idx--) {
if (newStats.belowCaps(gemCaps[numPasses][1])) {
break;
}

const [itemSlot, socketIdx] = socketList[idx];
updatedGear = updatedGear.withGem(itemSlot, socketIdx, nextGem);
await this.updateGear(updatedGear);
newStats = Stats.fromProto(this.player.getCurrentStats().finalStats);
}

// Now run a new pass to check whether we've exceeded the next stat cap
return await this.fillGemsToCaps(updatedGear, socketList, gemCaps, numPasses + 1, idx + 1);
}
}

0 comments on commit 8f29f77

Please sign in to comment.