Skip to content

Commit

Permalink
Merge pull request #39 from TheBackstabi/master
Browse files Browse the repository at this point in the history
Implement Mastery for UI (and some minor Rogue fixes)
  • Loading branch information
kayla-glick authored Mar 29, 2024
2 parents b970dda + d168dc5 commit 20ec26d
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 52 deletions.
4 changes: 2 additions & 2 deletions sim/rogue/assassination/sealfate.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func (sinRogue *AssassinationRogue) applySealFate() {
return
}

procChance := 0.2 * float64(sinRogue.Talents.SealFate)
procChance := 0.5 * float64(sinRogue.Talents.SealFate)
cpMetrics := sinRogue.NewComboPointMetrics(core.ActionID{SpellID: 14190})

icd := core.Cooldown{
Expand All @@ -31,7 +31,7 @@ func (sinRogue *AssassinationRogue) applySealFate() {
return
}

if icd.IsReady(sim) && sim.Proc(procChance, "Seal Fate") {
if icd.IsReady(sim) && (procChance == 1 || sim.Proc(procChance, "Seal Fate")) {
sinRogue.AddComboPoints(sim, 1, cpMetrics)
icd.Use(sim)
}
Expand Down
12 changes: 4 additions & 8 deletions sim/rogue/subtlety/honor_among_thieves.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,19 @@ func (subRogue *SubtletyRogue) registerHonorAmongThieves() {

icd := core.Cooldown{
Timer: subRogue.NewTimer(),
Duration: time.Second,
Duration: time.Second * 2,
}

maybeProc := func(sim *core.Simulation) {
if icd.IsReady(sim) && sim.Proc(procChance, "honor of thieves") {
if icd.IsReady(sim) && sim.Proc(procChance, "Honor Among Thieves") {
subRogue.AddComboPoints(sim, 1, comboMetrics)
icd.Use(sim)
}
}

subRogue.HonorAmongThieves = subRogue.RegisterAura(core.Aura{
subRogue.HonorAmongThieves = core.MakePermanent(subRogue.RegisterAura(core.Aura{
Label: "Honor Among Thieves",
ActionID: honorAmongThievesID,
Duration: core.NeverExpires,
OnReset: func(aura *core.Aura, sim *core.Simulation) {
aura.Activate(sim)
},
OnGain: func(_ *core.Aura, sim *core.Simulation) {
// In an ideal party, you'd probably get up to 6 ability crits/s (Rate = 600).
// Survival Hunters, Enhancement Shamans, and Assassination Rogues are particularly good.
Expand Down Expand Up @@ -69,5 +65,5 @@ func (subRogue *SubtletyRogue) registerHonorAmongThieves() {
maybeProc(sim)
}
},
})
}))
}
2 changes: 1 addition & 1 deletion sim/rogue/subtlety/subtlety.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/wowsims/cata/sim/rogue"
)

const masteryDamagePerPoint = .01
const masteryDamagePerPoint = .025
const masteryBaseEffect = 0.2

func RegisterSubtletyRogue() {
Expand Down
70 changes: 49 additions & 21 deletions ui/core/components/character_stats.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Popover, Tooltip } from 'bootstrap';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { element, fragment } from 'tsx-vanilla';
import { element, fragment, ref } from 'tsx-vanilla';

import { Player } from '..//player.js';
import { Class, PseudoStat, Stat, TristateEffect } from '..//proto/common.js';
import { getClassStatName, statOrder } from '..//proto_utils/names.js';
import { getClassStatName, masterySpellIDs, masterySpellNames, statOrder } from '..//proto_utils/names.js';
import { Stats } from '..//proto_utils/stats.js';
import { EventID, TypedEvent } from '..//typed_event.js';
import * as Mechanics from '../constants/mechanics.js';
import { ActionId } from '../proto_utils/action_id';
import { Component } from './component.js';
import { NumberPicker } from './number_picker';

Expand All @@ -17,6 +18,7 @@ export class CharacterStats extends Component {
readonly stats: Array<Stat>;
readonly valueElems: Array<HTMLTableCellElement>;
readonly meleeCritCapValueElem: HTMLTableCellElement | undefined;
masteryElem: HTMLTableCellElement | undefined;

private readonly player: Player<any>;
private readonly modifyDisplayStats?: (player: Player<any>) => StatMods;
Expand All @@ -42,7 +44,15 @@ export class CharacterStats extends Component {

const row = (
<tr className="character-stats-table-row">
<td className="character-stats-table-label">{statName}</td>
<td className="character-stats-table-label">
{statName}
{stat == Stat.StatMastery && (
<>
<br />
{masterySpellNames.get(this.player.getSpec())}
</>
)}
</td>
<td className="character-stats-table-value">{this.bonusStatsLink(stat)}</td>
</tr>
);
Expand Down Expand Up @@ -97,32 +107,48 @@ export class CharacterStats extends Component {
const consumesDelta = consumesStats.subtract(buffsStats);

const finalStats = Stats.fromProto(playerStats.finalStats).add(statMods.talents).add(debuffStats);
const masteryPoints =
this.player.getBaseMastery() + (playerStats.finalStats?.stats[Stat.StatMastery] || 0) / Mechanics.MASTERY_RATING_PER_MASTERY_POINT;

this.stats.forEach((stat, idx) => {
const valueElem = (
<a href="javascript:void(0)" className="stat-value-link" attributes={{ role: 'button' }}>
{`${this.statDisplayString(finalStats, finalStats, stat)} `}
</a>
);

this.valueElems[idx].querySelector('.stat-value-link')?.remove();
this.valueElems[idx].prepend(valueElem);

const bonusStatValue = bonusStats.getStat(stat);

let contextualKlass: string;
if (bonusStatValue == 0) {
valueElem.classList.add('text-white');
contextualKlass = 'text-white';
} else if (bonusStatValue > 0) {
valueElem.classList.add('text-success');
} else if (bonusStatValue < 0) {
valueElem.classList.add('text-danger');
contextualKlass = 'text-success';
} else {
contextualKlass = 'text-danger';
}

const statLinkElemRef = ref<HTMLAnchorElement>();

const valueElem = (
<div className="stat-value-link-container">
<a href="javascript:void(0)" className={`stat-value-link ${contextualKlass}`} attributes={{ role: 'button' }} ref={statLinkElemRef}>
{`${this.statDisplayString(finalStats, finalStats, stat, true)} `}
</a>
{stat == Stat.StatMastery && (
<a
href={ActionId.makeSpellUrl(masterySpellIDs.get(this.player.getSpec()) || 0)}
className={`stat-value-link-mastery ${contextualKlass}`}
attributes={{ role: 'button' }}>
{`${(masteryPoints * this.player.getMasteryPerPointModifier()).toFixed(2)}%`}
</a>
)}
</div>
);

const statLinkElem = statLinkElemRef.value!;

this.valueElems[idx].querySelector('.stat-value-link-container')?.remove();
this.valueElems[idx].prepend(valueElem);

const tooltipContent = (
<div>
<div className="character-stats-tooltip-row">
<span>Base:</span>
<span>{this.statDisplayString(baseStats, baseDelta, stat)}</span>
<span>{this.statDisplayString(baseStats, baseDelta, stat, true)}</span>
</div>
<div className="character-stats-tooltip-row">
<span>Gear:</span>
Expand Down Expand Up @@ -154,11 +180,11 @@ export class CharacterStats extends Component {
)}
<div className="character-stats-tooltip-row">
<span>Total:</span>
<span>{this.statDisplayString(finalStats, finalStats, stat)}</span>
<span>{this.statDisplayString(finalStats, finalStats, stat, true)}</span>
</div>
</div>
);
Tooltip.getOrCreateInstance(valueElem, {
Tooltip.getOrCreateInstance(statLinkElem, {
title: tooltipContent,
html: true,
});
Expand Down Expand Up @@ -232,7 +258,7 @@ export class CharacterStats extends Component {
}
}

private statDisplayString(stats: Stats, deltaStats: Stats, stat: Stat): string {
private statDisplayString(stats: Stats, deltaStats: Stats, stat: Stat, includeBase?: boolean): string {
let rawValue = deltaStats.getStat(stat);

if (stat == Stat.StatBlockValue) {
Expand Down Expand Up @@ -280,6 +306,8 @@ export class CharacterStats extends Component {
displayStr += ` (${(stats.getPseudoStat(PseudoStat.PseudoStatParry) * 100).toFixed(2)}%)`;
} else if (stat == Stat.StatResilience) {
displayStr += ` (${(rawValue / Mechanics.RESILIENCE_RATING_PER_CRIT_REDUCTION_CHANCE).toFixed(2)}%)`;
} else if (stat == Stat.StatMastery) {
displayStr += ` (${(rawValue / Mechanics.MASTERY_RATING_PER_MASTERY_POINT + (includeBase ? this.player.getBaseMastery() : 0)).toFixed(2)} Points)`;
}

return displayStr;
Expand Down
44 changes: 43 additions & 1 deletion ui/core/constants/mechanics.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Spec } from "../proto/common";

export const CHARACTER_LEVEL = 85;
export const BOSS_LEVEL = CHARACTER_LEVEL + 3;

Expand All @@ -10,7 +12,7 @@ export const SPELL_CRIT_RATING_PER_CRIT_CHANCE = 179.28;
export const SPELL_HIT_RATING_PER_HIT_CHANCE = 102.44;

export const HASTE_RATING_PER_HASTE_PERCENT = 128.06;
export const MasterRatingPerMasterPecent = 179.28;
export const MASTERY_RATING_PER_MASTERY_POINT = 179.28;

// Shamans, Paladins, Druids, Death Knights get more haste than everyone else, for melee.
export const SPECIAL_MELEE_HASTE_RATING_PER_HASTE_PERCENT = 25.22;
Expand All @@ -22,3 +24,43 @@ export const DODGE_RATING_PER_DODGE_CHANCE = 176.71;
export const PARRY_RATING_PER_PARRY_CHANCE = 176.71;
export const RESILIENCE_RATING_PER_CRIT_REDUCTION_CHANCE = 0;
export const RESILIENCE_RATING_PER_CRIT_DAMAGE_REDUCTION_PERCENT = 94.27 / 2.2;

// Mastery Ratings have various increments based on spec.
export const masteryPercentPerPoint: Map<Spec, number> = new Map([
[Spec.SpecAssassinationRogue, 3.5],
[Spec.SpecCombatRogue, 2.0],
[Spec.SpecSubtletyRogue, 2.5],
[Spec.SpecBloodDeathKnight, 6.25],
[Spec.SpecFrostDeathKnight, 2.0],
[Spec.SpecUnholyDeathKnight, 2.5],
[Spec.SpecBalanceDruid, 2.0],
[Spec.SpecFeralDruid, 0],
[Spec.SpecRestorationDruid, 1.25],
[Spec.SpecHolyPaladin, 1.5],
[Spec.SpecProtectionPaladin, 2.25],
[Spec.SpecRetributionPaladin, 2.1],
[Spec.SpecElementalShaman, 2.0],
[Spec.SpecEnhancementShaman, 2.5],
[Spec.SpecRestorationShaman, 3.0],
[Spec.SpecBeastMasteryHunter, 1.675],
[Spec.SpecMarksmanshipHunter, 2.1],
[Spec.SpecSurvivalHunter, 1.0],
[Spec.SpecArmsWarrior, 2.2],
[Spec.SpecFuryWarrior, 5.6],
[Spec.SpecProtectionWarrior, 1.5],
[Spec.SpecArcaneMage, 1.5],
[Spec.SpecFireMage, 2.8],
[Spec.SpecFrostMage, 2.5],
[Spec.SpecDisciplinePriest, 2.5],
[Spec.SpecHolyPriest, 1.25],
[Spec.SpecShadowPriest, 1.45],
[Spec.SpecAfflictionWarlock, 1.625],
[Spec.SpecDemonologyWarlock, 2.3],
[Spec.SpecDestructionWarlock, 1.35],
]);

// Pending split of Feral to Cat/Bear specifically
export const MASTERY_DRUID_BEAR_PER_POINT = 4.0;
export const MASTERY_DRUID_CAT_PER_POINT = 3.125;


14 changes: 14 additions & 0 deletions ui/core/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1518,4 +1518,18 @@ export class Player<SpecType extends Spec> {
);
});
}

getBaseMastery(): number {
switch(this.playerSpec.specID) {
case Spec.SpecFrostMage:
case Spec.SpecFuryWarrior:
return 2;
default:
return 8;
}
}

getMasteryPerPointModifier(): number {
return Mechanics.masteryPercentPerPoint.get(this.getSpec()) || 0
}
}
70 changes: 69 additions & 1 deletion ui/core/proto_utils/names.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ResourceType } from '../proto/api.js';
import { ArmorType, Class, ItemSlot, Profession, PseudoStat, Race, RangedWeaponType, Stat, WeaponType } from '../proto/common.js';
import { ArmorType, Class, ItemSlot, Profession, PseudoStat, Race, RangedWeaponType, Spec, Stat, WeaponType } from '../proto/common.js';
import { DungeonDifficulty, RaidFilterOption, SourceFilterOption } from '../proto/ui.js';

export const armorTypeNames: Map<ArmorType, string> = new Map([
Expand Down Expand Up @@ -132,6 +132,7 @@ export const statOrder: Array<Stat> = [
Stat.StatMeleeHaste,
Stat.StatArmorPenetration,
Stat.StatExpertise,
Stat.StatMastery,
Stat.StatDefense,
Stat.StatBlock,
Stat.StatBlockValue,
Expand Down Expand Up @@ -162,6 +163,7 @@ export const statNames: Map<Stat, string> = new Map([
[Stat.StatMeleeCrit, 'Melee Crit'],
[Stat.StatMeleeHaste, 'Melee Haste'],
[Stat.StatArmorPenetration, 'Armor Pen'],
[Stat.StatMastery, 'Mastery'],
[Stat.StatExpertise, 'Expertise'],
[Stat.StatMana, 'Mana'],
[Stat.StatArmor, 'Armor'],
Expand Down Expand Up @@ -322,3 +324,69 @@ export const difficultyNames: Map<DungeonDifficulty, string> = new Map([
[DungeonDifficulty.DifficultyRaid25, '25N'],
[DungeonDifficulty.DifficultyRaid25H, '25H'],
]);

export const masterySpellNames: Map<Spec, string> = new Map([
[Spec.SpecAssassinationRogue, 'Potent Poisons'],
[Spec.SpecCombatRogue, 'Main Gauche'],
[Spec.SpecSubtletyRogue, 'Executioner'],
[Spec.SpecBloodDeathKnight, 'Blood Shield'],
[Spec.SpecFrostDeathKnight, 'Frozen Heart'],
[Spec.SpecUnholyDeathKnight, 'Dreadblade'],
[Spec.SpecBalanceDruid, 'Total Eclipse'],
[Spec.SpecFeralDruid, 'No'],
[Spec.SpecRestorationDruid, 'Harmony'],
[Spec.SpecHolyPaladin, 'Illuminated Healing'],
[Spec.SpecProtectionPaladin, 'Divine Bulwark'],
[Spec.SpecRetributionPaladin, 'Hand of Light'],
[Spec.SpecElementalShaman, 'Elemental Overload'],
[Spec.SpecEnhancementShaman, 'Enhanced Elements'],
[Spec.SpecRestorationShaman, 'Deep Healing'],
[Spec.SpecBeastMasteryHunter, 'Master of Beasts'],
[Spec.SpecMarksmanshipHunter, 'Wild Quiver'],
[Spec.SpecSurvivalHunter, 'Essence of the Viper'],
[Spec.SpecArmsWarrior, 'Strikes of Opportunity'],
[Spec.SpecFuryWarrior, 'Unshackled Fury'],
[Spec.SpecProtectionWarrior, 'Critical Block'],
[Spec.SpecArcaneMage, 'Mana Adept'],
[Spec.SpecFireMage, 'Flashburn'],
[Spec.SpecFrostMage, 'Frostburn'],
[Spec.SpecDisciplinePriest, 'Shield Discipline'],
[Spec.SpecHolyPriest, 'Echo of Light'],
[Spec.SpecShadowPriest, 'Shadow Orb Power'],
[Spec.SpecAfflictionWarlock, 'Potent Afflictions'],
[Spec.SpecDemonologyWarlock, 'Master Demonologist'],
[Spec.SpecDestructionWarlock, 'Fiery Apocalypse'],
]);

export const masterySpellIDs: Map<Spec, number> = new Map([
[Spec.SpecAssassinationRogue, 76803],
[Spec.SpecCombatRogue, 76806],
[Spec.SpecSubtletyRogue, 76808],
[Spec.SpecBloodDeathKnight, 77513],
[Spec.SpecFrostDeathKnight, 77514],
[Spec.SpecUnholyDeathKnight, 77515],
[Spec.SpecBalanceDruid, 77492],
[Spec.SpecFeralDruid, 0],
[Spec.SpecRestorationDruid, 77495],
[Spec.SpecHolyPaladin, 76669],
[Spec.SpecProtectionPaladin, 76671],
[Spec.SpecRetributionPaladin, 76672],
[Spec.SpecElementalShaman, 77222],
[Spec.SpecEnhancementShaman, 77223],
[Spec.SpecRestorationShaman, 77226],
[Spec.SpecBeastMasteryHunter, 76657],
[Spec.SpecMarksmanshipHunter, 76659],
[Spec.SpecSurvivalHunter, 76658],
[Spec.SpecArmsWarrior, 76838],
[Spec.SpecFuryWarrior, 76856],
[Spec.SpecProtectionWarrior, 76857],
[Spec.SpecArcaneMage, 76547],
[Spec.SpecFireMage, 76595],
[Spec.SpecFrostMage, 76613],
[Spec.SpecDisciplinePriest, 77484],
[Spec.SpecHolyPriest, 77485],
[Spec.SpecShadowPriest, 77486],
[Spec.SpecAfflictionWarlock, 77215],
[Spec.SpecDemonologyWarlock, 77219],
[Spec.SpecDestructionWarlock, 77220],
]);
6 changes: 3 additions & 3 deletions ui/rogue/assassination/sim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ const SPEC_CONFIG = registerSpecConfig(Spec.SpecAssassinationRogue, {
Stat.StatSpellHit,
Stat.StatSpellCrit,
Stat.StatMeleeHaste,
Stat.StatArmorPenetration,
Stat.StatMastery,
Stat.StatExpertise,
],
epPseudoStats: [PseudoStat.PseudoStatMainHandDps, PseudoStat.PseudoStatOffHandDps],
Expand All @@ -181,7 +181,7 @@ const SPEC_CONFIG = registerSpecConfig(Spec.SpecAssassinationRogue, {
Stat.StatMeleeCrit,
Stat.StatSpellCrit,
Stat.StatMeleeHaste,
Stat.StatArmorPenetration,
Stat.StatMastery,
Stat.StatExpertise,
],

Expand All @@ -199,7 +199,7 @@ const SPEC_CONFIG = registerSpecConfig(Spec.SpecAssassinationRogue, {
[Stat.StatMeleeHit]: 1.39,
[Stat.StatMeleeCrit]: 1.32,
[Stat.StatMeleeHaste]: 1.48,
[Stat.StatArmorPenetration]: 0.84,
[Stat.StatMastery]: 0.84,
[Stat.StatExpertise]: 0.98,
},
{
Expand Down
Loading

0 comments on commit 20ec26d

Please sign in to comment.