diff --git a/ui/core/components/character_stats.tsx b/ui/core/components/character_stats.tsx index e9e53c7e0..fa11dcbd1 100644 --- a/ui/core/components/character_stats.tsx +++ b/ui/core/components/character_stats.tsx @@ -3,7 +3,7 @@ import { ref } from 'tsx-vanilla'; import * as Mechanics from '../constants/mechanics.js'; import { Player } from '../player.js'; -import { PseudoStat, Spec, Stat } from '../proto/common.js'; +import { HandType, ItemSlot, PseudoStat, Spec, Stat, WeaponType } from '../proto/common.js'; import { Stats, UnitStat } from '../proto_utils/stats.js'; import { EventID, TypedEvent } from '../typed_event.js'; import { Component } from './component.js'; @@ -12,13 +12,7 @@ import { NumberPicker } from './number_picker'; export type StatMods = { talents?: Stats; buffs?: Stats }; const statGroups = new Map>([ - [ - 'Primary', - [ - UnitStat.fromStat(Stat.StatHealth), - UnitStat.fromStat(Stat.StatMana), - ], - ], + ['Primary', [UnitStat.fromStat(Stat.StatHealth), UnitStat.fromStat(Stat.StatMana)]], [ 'Attributes', [ @@ -27,7 +21,7 @@ const statGroups = new Map>([ UnitStat.fromStat(Stat.StatStamina), UnitStat.fromStat(Stat.StatIntellect), UnitStat.fromStat(Stat.StatSpirit), - ] + ], ], [ 'Physical', @@ -40,7 +34,7 @@ const statGroups = new Map>([ UnitStat.fromStat(Stat.StatMeleeCrit), UnitStat.fromStat(Stat.StatMeleeHaste), UnitStat.fromPseudoStat(PseudoStat.BonusPhysicalDamage), - ] + ], ], [ 'Spell', @@ -58,7 +52,7 @@ const statGroups = new Map>([ UnitStat.fromStat(Stat.StatSpellHaste), UnitStat.fromStat(Stat.StatSpellPenetration), UnitStat.fromStat(Stat.StatMP5), - ] + ], ], [ 'Defense', @@ -70,7 +64,7 @@ const statGroups = new Map>([ UnitStat.fromStat(Stat.StatParry), UnitStat.fromStat(Stat.StatBlock), UnitStat.fromStat(Stat.StatBlockValue), - ] + ], ], [ 'Resistance', @@ -80,13 +74,10 @@ const statGroups = new Map>([ UnitStat.fromStat(Stat.StatFrostResistance), UnitStat.fromStat(Stat.StatNatureResistance), UnitStat.fromStat(Stat.StatShadowResistance), - ] - ], - [ - 'Misc', - [] + ], ], -]) + ['Misc', []], +]); export class CharacterStats extends Component { readonly stats: Array; @@ -247,46 +238,51 @@ export class CharacterStats extends Component {
Axes - - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatAxesSkill)} /{' '} - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatTwoHandedAxesSkill)} - + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatAxesSkill)} +
+
+ 2H Axes + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatTwoHandedAxesSkill)}
Daggers - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatDaggersSkill)} + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatDaggersSkill)}
+ {/* Commenting out feral combat skill since not present in Classic. {player.spec === Spec.SpecFeralDruid && (
Feral Combat - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatFeralCombatSkill)} + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatFeralCombatSkill)}
)} + */}
Maces - - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatMacesSkill)} /{' '} - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatTwoHandedMacesSkill)} - + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatMacesSkill)} +
+
+ 2H Maces + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatTwoHandedMacesSkill)}
Polearms - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatPolearmsSkill)} + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatPolearmsSkill)}
Staves - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatStavesSkill)} + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatStavesSkill)}
Swords - - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatSwordsSkill)} /{' '} - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatTwoHandedSwordsSkill)} - + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatSwordsSkill)} +
+
+ 2H Swords + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatTwoHandedSwordsSkill)}
Unarmed - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatUnarmedSkill)} + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatUnarmedSkill)}
, ); @@ -327,15 +323,27 @@ export class CharacterStats extends Component { }); if (this.meleeCritCapValueElem) { - const meleeCritCapInfo = player.getMeleeCritCapInfo(); + const has2hWeapon = player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.handType == HandType.HandTypeTwoHand; + const mhWeapon: WeaponType = player.getGear().getEquippedItem(ItemSlot.ItemSlotMainHand)?.item.weaponType as WeaponType; + const ohWeapon: WeaponType = player.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType as WeaponType; + const mhCritCapInfo = player.getMeleeCritCapInfo(mhWeapon, has2hWeapon); + const ohCritCapInfo = player.getMeleeCritCapInfo(ohWeapon, has2hWeapon); + + const playerCritCapDelta = mhCritCapInfo.playerCritCapDelta; + + if (playerCritCapDelta === 0.0) { + const prefix = 'Exact'; + } + + const prefix = playerCritCapDelta > 0 ? 'Over by ' : 'Under by '; const valueElem = ( - {`${this.meleeCritCapDisplayString(player, finalStats)} `} + {`${prefix} ${Math.abs(playerCritCapDelta).toFixed(2)}%`} ); - const capDelta = meleeCritCapInfo.playerCritCapDelta; + const capDelta = mhCritCapInfo.playerCritCapDelta; if (capDelta === 0) { valueElem.classList.add('text-white'); } else if (capDelta > 0) { @@ -349,41 +357,90 @@ export class CharacterStats extends Component { const tooltipContent = (
+
+ Main Hand + +
+
Glancing: - {`${meleeCritCapInfo.glancing.toFixed(2)}%`} + {`${mhCritCapInfo.glancing.toFixed(2)}%`}
Suppression: - {`${meleeCritCapInfo.suppression.toFixed(2)}%`} + {`${mhCritCapInfo.suppression.toFixed(2)}%`}
- To Hit Cap: - {`${meleeCritCapInfo.remainingMeleeHitCap.toFixed(2)}%`} + White Miss: + {`${mhCritCapInfo.remainingMeleeHitCap.toFixed(2)}%`}
- To Exp Cap: - {`${meleeCritCapInfo.remainingExpertiseCap.toFixed(2)}%`} + Dodge: + {`${mhCritCapInfo.dodgeCap.toFixed(2)}%`}
- Debuffs: - {`${meleeCritCapInfo.debuffCrit.toFixed(2)}%`} + Parry: + {`${mhCritCapInfo.parryCap.toFixed(2)}%`}
- {meleeCritCapInfo.specSpecificOffset != 0 && ( + {mhCritCapInfo.specSpecificOffset != 0 && (
Spec Offsets: - {`${meleeCritCapInfo.specSpecificOffset.toFixed(2)}%`} + {`${mhCritCapInfo.specSpecificOffset.toFixed(2)}%`}
)}
Final Crit Cap: - {`${meleeCritCapInfo.baseCritCap.toFixed(2)}%`} + {`${mhCritCapInfo.baseCritCap.toFixed(2)}%`}
-
Can Raise By: - {`${(meleeCritCapInfo.remainingExpertiseCap + meleeCritCapInfo.remainingMeleeHitCap).toFixed(2)}%`} + {`${mhCritCapInfo.remainingMeleeHitCap.toFixed(2)}%`}
+ {!has2hWeapon && ( +
+
+ +
+ Off Hand + +
+
+
+ Glancing: + {`${ohCritCapInfo.glancing.toFixed(2)}%`} +
+
+ Suppression: + {`${ohCritCapInfo.suppression.toFixed(2)}%`} +
+
+ White Miss: + {`${ohCritCapInfo.remainingMeleeHitCap.toFixed(2)}%`} +
+
+ Dodge: + {`${ohCritCapInfo.dodgeCap.toFixed(2)}%`} +
+
+ Parry: + {`${ohCritCapInfo.parryCap.toFixed(2)}%`} +
+ {ohCritCapInfo.specSpecificOffset != 0 && ( +
+ Spec Offsets: + {`${ohCritCapInfo.specSpecificOffset.toFixed(2)}%`} +
+ )} +
+ Final Crit Cap: + {`${ohCritCapInfo.baseCritCap.toFixed(2)}%`} +
+
+ Can Raise By: + {`${ohCritCapInfo.remainingMeleeHitCap.toFixed(2)}%`} +
+
+ )}
); @@ -403,7 +460,7 @@ export class CharacterStats extends Component { if (stat === Stat.StatBlockValue) { const mult = stats.getPseudoStat(PseudoStat.PseudoStatBlockValueMultiplier) || 1; const perStr = Math.max(0, stats.getPseudoStat(PseudoStat.PseudoStatBlockValuePerStrength) * deltaStats.getStat(Stat.StatStrength) - 1); - displayStr = String(Math.round((rawValue * mult) + perStr)); + displayStr = String(Math.round(rawValue * mult + perStr)); } else if (stat === Stat.StatMeleeHit) { displayStr = `${(rawValue / Mechanics.MELEE_HIT_RATING_PER_HIT_CHANCE).toFixed(2)}%`; } else if (stat === Stat.StatSpellHit) { @@ -447,7 +504,7 @@ export class CharacterStats extends Component { displayStr = `${rawValue} (${(rawValue / Mechanics.RESILIENCE_RATING_PER_CRIT_REDUCTION_CHANCE).toFixed(2)}%)`; } } - + if (!displayStr) displayStr = String(Math.round(rawValue)); return displayStr; @@ -516,19 +573,6 @@ export class CharacterStats extends Component { } private shouldShowMeleeCritCap(player: Player): boolean { - // TODO: Disabled for now while we fix displayed crit cap - return false; - // return [Spec.SpecEnhancementShaman, Spec.SpecRetributionPaladin, Spec.SpecRogue, Spec.SpecWarrior, Spec.SpecHunter].includes(player.spec); - } - - private meleeCritCapDisplayString(player: Player, _: Stats): string { - const playerCritCapDelta = player.getMeleeCritCap(); - - if (playerCritCapDelta === 0.0) { - return 'Exact'; - } - - const prefix = playerCritCapDelta > 0 ? 'Over by ' : 'Under by '; - return `${prefix} ${Math.abs(playerCritCapDelta).toFixed(2)}%`; + return [Spec.SpecEnhancementShaman, Spec.SpecRetributionPaladin, Spec.SpecRogue, Spec.SpecWarrior, Spec.SpecHunter].includes(player.spec); } } diff --git a/ui/core/components/encounter_picker.ts b/ui/core/components/encounter_picker.ts index feabb5074..829de2723 100644 --- a/ui/core/components/encounter_picker.ts +++ b/ui/core/components/encounter_picker.ts @@ -320,18 +320,6 @@ class TargetPicker extends Input { { name: '62', value: 62 }, { name: '61', value: 61 }, { name: '60', value: 60 }, - { name: '53', value: 53 }, - { name: '52', value: 52 }, - { name: '51', value: 51 }, - { name: '50', value: 50 }, - { name: '43', value: 43 }, - { name: '42', value: 42 }, - { name: '41', value: 41 }, - { name: '40', value: 40 }, - { name: '28', value: 28 }, - { name: '27', value: 27 }, - { name: '26', value: 26 }, - { name: '25', value: 25 }, ], changedEvent: () => encounter.targetsChangeEmitter, getValue: () => this.getTarget().level, diff --git a/ui/core/player.ts b/ui/core/player.ts index 7ebe0aba4..0c31b902d 100644 --- a/ui/core/player.ts +++ b/ui/core/player.ts @@ -30,6 +30,7 @@ import { Stat, UnitReference, UnitStats, + WeaponType, } from './proto/common.js'; import { DungeonFilterOption, @@ -176,14 +177,14 @@ export interface MeleeCritCapInfo { meleeCrit: number; meleeHit: number; expertise: number; - suppression: number; glancing: number; + suppression: number; debuffCrit: number; hasOffhandWeapon: boolean; meleeHitCap: number; - expertiseCap: number; remainingMeleeHitCap: number; - remainingExpertiseCap: number; + dodgeCap: number; + parryCap: number; baseCritCap: number; specSpecificOffset: number; playerCritCapDelta: number; @@ -688,20 +689,71 @@ export class Player { this.bonusStatsChangeEmitter.emit(eventID); } - getMeleeCritCapInfo(): MeleeCritCapInfo { + getMeleeCritCapInfo(weapon: WeaponType, has2hWeapon: boolean): MeleeCritCapInfo { + let targetLevel = 63; // Initializes at level 63 until UI is loaded + if (this.sim.encounter.targets) { + targetLevel = this.sim.encounter?.primaryTarget.level; + } + const levelDiff = targetLevel - Mechanics.MAX_CHARACTER_LEVEL; + const defenderDefense = targetLevel * 5; + const glancing = (1 + levelDiff) * 10.0; + const suppression = levelDiff === 3 ? levelDiff + 1.8 : levelDiff; + + let weaponSkill = 300.0; const meleeCrit = (this.currentStats.finalStats?.stats[Stat.StatMeleeCrit] || 0.0) / Mechanics.MELEE_CRIT_RATING_PER_CRIT_CHANCE; const meleeHit = (this.currentStats.finalStats?.stats[Stat.StatMeleeHit] || 0.0) / Mechanics.MELEE_HIT_RATING_PER_HIT_CHANCE; const expertise = (this.currentStats.finalStats?.stats[Stat.StatExpertise] || 0.0) / Mechanics.EXPERTISE_PER_QUARTER_PERCENT_REDUCTION / 4; - const suppression = 4.8; - const glancing = 40.0; + const hasOffhandWeapon = this.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponType !== undefined; + + const getWeaponSkillForWeaponType = (skill: PseudoStat) => this.currentStats.talentsStats?.pseudoStats[skill] || 0.0; + + if (!has2hWeapon) { + switch (weapon) { + case WeaponType.WeaponTypeUnknown: + break; + case WeaponType.WeaponTypeAxe: + weaponSkill += getWeaponSkillForWeaponType(PseudoStat.PseudoStatAxesSkill); + break; + case WeaponType.WeaponTypeDagger: + weaponSkill += getWeaponSkillForWeaponType(PseudoStat.PseudoStatDaggersSkill); + break; + case WeaponType.WeaponTypeFist: + weaponSkill += getWeaponSkillForWeaponType(PseudoStat.PseudoStatUnarmedSkill); + break; + case WeaponType.WeaponTypeMace: + weaponSkill += getWeaponSkillForWeaponType(PseudoStat.PseudoStatMacesSkill); + break; + case WeaponType.WeaponTypeSword: + weaponSkill += getWeaponSkillForWeaponType(PseudoStat.PseudoStatSwordsSkill); + break; + } + } + if (has2hWeapon) { + switch (weapon) { + case WeaponType.WeaponTypeUnknown: + break; + case WeaponType.WeaponTypeAxe: + weaponSkill += getWeaponSkillForWeaponType(PseudoStat.PseudoStatTwoHandedAxesSkill); + break; + case WeaponType.WeaponTypeMace: + weaponSkill += getWeaponSkillForWeaponType(PseudoStat.PseudoStatTwoHandedMacesSkill); + break; + case WeaponType.WeaponTypeSword: + weaponSkill += getWeaponSkillForWeaponType(PseudoStat.PseudoStatTwoHandedSwordsSkill); + break; + } + } - const hasOffhandWeapon = this.getGear().getEquippedItem(ItemSlot.ItemSlotOffHand)?.item.weaponSpeed !== undefined; - // Due to warrior HS bug, hit cap for crit cap calculation should be 8% instead of 27% - const meleeHitCap = hasOffhandWeapon && this.spec !== Spec.SpecWarrior ? 27.0 : 8.0; - const dodgeCap = 6.5; - const parryCap = this.getInFrontOfTarget() ? 14.0 : 0; - const expertiseCap = dodgeCap + parryCap; + const skillDiff = defenderDefense - weaponSkill; + // Due to warrior HS bug, hit cap for crit cap calculation ignores the 19% penalty + let meleeHitCap = skillDiff <= 10 ? 5.0 + skillDiff * 0.1 : 5.0 + skillDiff * 0.2 + (skillDiff - 10) * 0.2; + meleeHitCap = hasOffhandWeapon && this.spec !== Spec.SpecWarrior ? meleeHitCap + 19.0 : meleeHitCap + 0.0; + const dodgeCap = 5.0 + skillDiff * 0.1; + let parryCap = 0.0; + if (this.getInFrontOfTarget()) { + parryCap = levelDiff === 3 ? 14.0 : 5.0 + skillDiff * 0.1; // 14% parry at +3 level and follows dodge scaling otherwise + } const remainingMeleeHitCap = Math.max(meleeHitCap - meleeHit, 0.0); const remainingDodgeCap = Math.max(dodgeCap - expertise, 0.0); const remainingParryCap = Math.max(parryCap - expertise, 0.0); @@ -726,24 +778,20 @@ export class Player { meleeCrit, meleeHit, expertise, - suppression, glancing, + suppression, debuffCrit, hasOffhandWeapon, meleeHitCap, - expertiseCap, - remainingMeleeHitCap, - remainingExpertiseCap, + dodgeCap, + parryCap, baseCritCap, specSpecificOffset, playerCritCapDelta, + remainingMeleeHitCap, }; } - getMeleeCritCap() { - return this.getMeleeCritCapInfo().playerCritCapDelta; - } - getSimpleRotation(): SpecRotation { const jsonStr = this.aplRotation.simple?.specRotationJson || ''; if (!jsonStr) { @@ -1112,7 +1160,7 @@ export class Player { filterItemData(itemData: Array, getItemFunc: (val: T) => Item, slot: ItemSlot): Array { const filters = this.sim.getFilters(); - + const filterItems = (itemData: Array, filterFunc: (item: Item) => boolean) => { return itemData.filter(itemElem => filterFunc(getItemFunc(itemElem))); }; @@ -1150,7 +1198,7 @@ export class Player { const zoneId = DungeonFilterOption[zoneName]; if (typeof zoneId === 'number' && zoneId !== 0 && !filters.raids.includes(zoneId)) { - zoneIds.push(zoneId) + zoneIds.push(zoneId); } } @@ -1167,7 +1215,7 @@ export class Player { const zoneId = RaidFilterOption[zoneName]; if (typeof zoneId === 'number' && zoneId !== 0 && !filters.raids.includes(zoneId)) { - zoneIds.push(zoneId) + zoneIds.push(zoneId); } }