From e39088a2f6710486372d04adcede711c007fd964 Mon Sep 17 00:00:00 2001 From: sanguinerarogue Date: Wed, 8 Jan 2025 13:22:33 -0700 Subject: [PATCH] First pass at handedness crit cap for Classic --- ui/core/components/character_stats.tsx | 191 ++++++++++++++++--------- ui/core/player.ts | 86 ++++++++--- 2 files changed, 186 insertions(+), 91 deletions(-) diff --git a/ui/core/components/character_stats.tsx b/ui/core/components/character_stats.tsx index e9e53c7e0..4911e098a 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; @@ -248,45 +239,45 @@ export class CharacterStats extends Component {
Axes - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatAxesSkill)} /{' '} - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatTwoHandedAxesSkill)} + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatAxesSkill)} /{' '} + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatTwoHandedAxesSkill)}
Daggers - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatDaggersSkill)} + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatDaggersSkill)}
{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)} /{' '} + {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)} /{' '} + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatTwoHandedSwordsSkill)}
Unarmed - {this.weaponSkillDisplayString(gearStats, PseudoStat.PseudoStatUnarmedSkill)} + {this.weaponSkillDisplayString(talentsStats, PseudoStat.PseudoStatUnarmedSkill)}
, ); @@ -327,15 +318,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,40 +352,90 @@ export class CharacterStats extends Component { const tooltipContent = (
-
- Glancing: - {`${meleeCritCapInfo.glancing.toFixed(2)}%`} -
-
- Suppression: - {`${meleeCritCapInfo.suppression.toFixed(2)}%`} -
-
- To Hit Cap: - {`${meleeCritCapInfo.remainingMeleeHitCap.toFixed(2)}%`} -
-
- To Exp Cap: - {`${meleeCritCapInfo.remainingExpertiseCap.toFixed(2)}%`} -
-
- Debuffs: - {`${meleeCritCapInfo.debuffCrit.toFixed(2)}%`} -
- {meleeCritCapInfo.specSpecificOffset != 0 && ( +
+
+ Main Hand + +
+
+
+ Glancing: + {`${mhCritCapInfo.glancing.toFixed(2)}%`} +
+
+ Suppression: + {`${mhCritCapInfo.suppression.toFixed(2)}%`} +
+
+ White Miss: + {`${mhCritCapInfo.remainingMeleeHitCap.toFixed(2)}%`} +
+
+ Dodge: + {`${mhCritCapInfo.dodgeCap.toFixed(2)}%`} +
+
+ Parry: + {`${mhCritCapInfo.parryCap.toFixed(2)}%`} +
+ {mhCritCapInfo.specSpecificOffset != 0 && ( +
+ Spec Offsets: + {`${mhCritCapInfo.specSpecificOffset.toFixed(2)}%`} +
+ )} +
+ Final Crit Cap: + {`${mhCritCapInfo.baseCritCap.toFixed(2)}%`} +
+
- Spec Offsets: - {`${meleeCritCapInfo.specSpecificOffset.toFixed(2)}%`} + Can Raise By: + {`${mhCritCapInfo.remainingMeleeHitCap.toFixed(2)}%`}
- )} -
- Final Crit Cap: - {`${meleeCritCapInfo.baseCritCap.toFixed(2)}%`} +
-
-
- Can Raise By: - {`${(meleeCritCapInfo.remainingExpertiseCap + meleeCritCapInfo.remainingMeleeHitCap).toFixed(2)}%`} +
+
+ 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 +456,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 +500,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,12 +569,10 @@ 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); + return [Spec.SpecEnhancementShaman, Spec.SpecRetributionPaladin, Spec.SpecRogue, Spec.SpecWarrior, Spec.SpecHunter].includes(player.spec); } - private meleeCritCapDisplayString(player: Player, _: Stats): string { + /*private meleeCritCapDisplayString(player: Player, _: Stats): string { const playerCritCapDelta = player.getMeleeCritCap(); if (playerCritCapDelta === 0.0) { @@ -530,5 +581,5 @@ export class CharacterStats extends Component { const prefix = playerCritCapDelta > 0 ? 'Over by ' : 'Under by '; return `${prefix} ${Math.abs(playerCritCapDelta).toFixed(2)}%`; - } + }*/ } diff --git a/ui/core/player.ts b/ui/core/player.ts index 7ebe0aba4..5cc398fea 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, @@ -181,9 +182,9 @@ export interface MeleeCritCapInfo { 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,67 @@ export class Player { this.bonusStatsChangeEmitter.emit(eventID); } - getMeleeCritCapInfo(): MeleeCritCapInfo { + getMeleeCritCapInfo(weapon: WeaponType, has2hWeapon: boolean): MeleeCritCapInfo { + let defenderDefense = 315.0; // Initializes at level 63 until UI is loaded + if (this.sim.encounter.targets) { + const targetlevel = this.sim.encounter?.primaryTarget.level; + defenderDefense = targetlevel * 5; + } + const suppression = 4.8; + const glancing = 40.0; + + 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; + + if (!has2hWeapon) { + switch (weapon) { + case WeaponType.WeaponTypeUnknown: + break; + case WeaponType.WeaponTypeAxe: + weaponSkill += this.currentStats.talentsStats?.pseudoStats[PseudoStat.PseudoStatAxesSkill] || 0.0; + break; + case WeaponType.WeaponTypeDagger: + weaponSkill += this.currentStats.talentsStats?.pseudoStats[PseudoStat.PseudoStatDaggersSkill] || 0.0; + break; + case WeaponType.WeaponTypeFist: + weaponSkill += this.currentStats.talentsStats?.pseudoStats[PseudoStat.PseudoStatUnarmedSkill] || 0.0; + break; + case WeaponType.WeaponTypeMace: + weaponSkill += this.currentStats.talentsStats?.pseudoStats[PseudoStat.PseudoStatMacesSkill] || 0.0; + break; + case WeaponType.WeaponTypeSword: + weaponSkill += this.currentStats.talentsStats?.pseudoStats[PseudoStat.PseudoStatSwordsSkill] || 0.0; + break; + } + } + if (has2hWeapon) { + switch (weapon) { + case WeaponType.WeaponTypeUnknown: + break; + case WeaponType.WeaponTypeAxe: + weaponSkill += this.currentStats.talentsStats?.pseudoStats[PseudoStat.PseudoStatTwoHandedAxesSkill] || 0.0; + break; + case WeaponType.WeaponTypeMace: + weaponSkill += this.currentStats.talentsStats?.pseudoStats[PseudoStat.PseudoStatTwoHandedMacesSkill] || 0.0; + break; + case WeaponType.WeaponTypeSword: + weaponSkill += this.currentStats.talentsStats?.pseudoStats[PseudoStat.PseudoStatTwoHandedSwordsSkill] || 0.0; + 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; + // Due to warrior HS bug, hit cap for crit cap calculation ignores the 19% penalty + let meleeHitCap = + defenderDefense - weaponSkill <= 10 + ? 5.0 + (defenderDefense - weaponSkill) * 0.1 + : 5.0 + (defenderDefense - weaponSkill) * 0.2 + (defenderDefense - weaponSkill - 10) * 0.2; + meleeHitCap = hasOffhandWeapon && this.spec !== Spec.SpecWarrior ? meleeHitCap + 19.0 : meleeHitCap + 0.0; + const dodgeCap = 5.0 + (defenderDefense - weaponSkill) * 0.1; + const parryCap = this.getInFrontOfTarget() ? 14.0 : 0; const remainingMeleeHitCap = Math.max(meleeHitCap - meleeHit, 0.0); const remainingDodgeCap = Math.max(dodgeCap - expertise, 0.0); const remainingParryCap = Math.max(parryCap - expertise, 0.0); @@ -731,19 +779,15 @@ export class Player { 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 +1156,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 +1194,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 +1211,7 @@ export class Player { const zoneId = RaidFilterOption[zoneName]; if (typeof zoneId === 'number' && zoneId !== 0 && !filters.raids.includes(zoneId)) { - zoneIds.push(zoneId) + zoneIds.push(zoneId); } }