Skip to content

Update for MoP Hit Tables#69

Merged
1337LutZ merged 13 commits intomasterfrom
fix/mop-attack-table
May 19, 2025
Merged

Update for MoP Hit Tables#69
1337LutZ merged 13 commits intomasterfrom
fix/mop-attack-table

Conversation

@TheBackstabi
Copy link
Copy Markdown

@TheBackstabi TheBackstabi commented May 11, 2025

I'm kind of stupid so maybe missed something, but this should be right for updating Dodge/Parry to 7.5% and Spell Hit inheriting from Expertise and Hit Rating.

As far as I could tell looking through SimC and old posts, the logic for the rest of the table remained as-is.

@TheBackstabi
Copy link
Copy Markdown
Author

https://www.wowhead.com/news/new-ghostcrawler-blog-mists-of-pandaria-stat-changes-201233

I missed one thing - updating Block to be a second roll, after Miss/Dodge/Parry (assuming this applies to Mobs as well and not just players)

I need to sleep now so if someone else gets to that, go for it.

@TheBackstabi
Copy link
Copy Markdown
Author

TheBackstabi commented May 12, 2025

Need to update Enemy->Player block rolls

Crit suppression source: https://www.bluetracker.gg/wow/topic/us-en/5889309137-beta-class-balance-analysis/#97

Comment thread sim/core/spell_outcome.go Outdated
Comment on lines 853 to 859
func (spell *Spell) OutcomeExpectedMeleeWhite(_ *Simulation, result *SpellResult, attackTable *AttackTable) {
missChance := spell.GetPhysicalMissChance(attackTable)
dodgeChance := TernaryFloat64(spell.Flags.Matches(SpellFlagCannotBeDodged), 0, max(0, attackTable.BaseDodgeChance-spell.DodgeParrySuppression()-spell.Unit.PseudoStats.DodgeReduction))
parryChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, max(0, attackTable.BaseParryChance-spell.DodgeParrySuppression()), 0)
dodgeChance := TernaryFloat64(spell.Flags.Matches(SpellFlagCannotBeDodged), 0, max(0, attackTable.BaseDodgeChance-spell.DodgeSuppression()-spell.Unit.PseudoStats.DodgeReduction))
parryChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, max(0, attackTable.BaseParryChance-spell.ParrySuppression()), 0)
glanceChance := attackTable.BaseGlanceChance
blockChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, attackTable.BaseBlockChance, 0)
whiteCritCap := 1.0 - missChance - dodgeChance - parryChance - glanceChance - blockChance
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formulas for whiteCritCap and averageMultiplier also need to be updated here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crit cap makes sense, but I'm not sure there's anything needed for averageMultiplier...?

Comment thread sim/core/spell_outcome.go Outdated
Comment on lines 865 to 871
func (spell *Spell) OutcomeExpectedMeleeWeaponSpecialHitAndCrit(_ *Simulation, result *SpellResult, attackTable *AttackTable) {
missChance := max(0, attackTable.BaseMissChance-spell.PhysicalHitChance(attackTable))
dodgeChance := TernaryFloat64(spell.Flags.Matches(SpellFlagCannotBeDodged), 0, max(0, attackTable.BaseDodgeChance-spell.DodgeParrySuppression()-spell.Unit.PseudoStats.DodgeReduction))
parryChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, max(0, attackTable.BaseParryChance-spell.DodgeParrySuppression()), 0)
dodgeChance := TernaryFloat64(spell.Flags.Matches(SpellFlagCannotBeDodged), 0, max(0, attackTable.BaseDodgeChance-spell.DodgeSuppression()-spell.Unit.PseudoStats.DodgeReduction))
parryChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, max(0, attackTable.BaseParryChance-spell.ParrySuppression()), 0)
blockChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, attackTable.BaseBlockChance, 0)
critChance := spell.PhysicalCritChance(attackTable)
averageMultiplier := (1.0 - missChance - dodgeChance - parryChance) * (1.0 + (spell.CritDamageMultiplier()-1)*critChance)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these formulas should still hold though in MoP, right? Worth a double check.

Comment thread sim/core/spell_result.go Outdated
Comment on lines +99 to +100
parrySupp := expertiseRating / ExpertisePerQuarterPercentReduction / 400
parrySupp -= max(0, spell.DodgeSuppression())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a bug here: if the player has 10% Expertise then we should calculate 2.5% Parry suppression against a raid boss, but this formula will return 0 since spell.DodgeSuppression() will return 10%. I think this method will also need to take in an attackTable input to work properly in MoP, since you need access to attackTable.BaseDodgeChance and spell.Unit.PseudoStats.DodgeReduction in order to determine the correct amount of Expertise that was already "spent" on Dodge suppression.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll need exploration of what would use the DodgeReduction field, and if that actually impacts the Parry outcome too. I don't actually know/see anything interacting with that field anymore.

Comment thread sim/core/target.go
Comment thread sim/core/target.go Outdated
Comment on lines 246 to 247
if defender.Type == EnemyUnit {
// Assumes attacker (the Player) is level 80.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this comment, since I don't think there is any assumption required on player level for these calculations, just on the relative difference between the attacker and target levels.

Comment thread sim/core/target.go Outdated
Comment on lines 258 to 259
} else {
// Assumes defender (the Player) is level 80.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Comment thread sim/core/target.go Outdated
Comment on lines 258 to 260
} else {
// Assumes defender (the Player) is level 80.
table.BaseSpellMissChance = 0.05
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you checked the NPC-attacking-player values below as well for correctness in MoP?

Comment thread sim/core/unit.go
@InDebt
Copy link
Copy Markdown

InDebt commented May 13, 2025

https://www.engadget.com/2012-03-08-scattered-shots-hunter-expertise-in-mists-of-pandaria.html
https://wowcraftguides.weebly.com/hunter-guide-54-guide.html

Those blogs and guides indicate that Ranged attacks should now be dodgeable. So we might want to add that as well here?

@hillerstorm
Copy link
Copy Markdown

hillerstorm commented May 13, 2025

https://www.engadget.com/2012-03-08-scattered-shots-hunter-expertise-in-mists-of-pandaria.html https://wowcraftguides.weebly.com/hunter-guide-54-guide.html

Those blogs and guides indicate that Ranged attacks should now be dodgeable. So we might want to add that as well here?

Yep the default ranged outcome should include dodges and perhaps add one for NoDodge? If a spell matching that even exists

@TheBackstabi
Copy link
Copy Markdown
Author

TheBackstabi commented May 13, 2025

Just leaving more notes as I research things in between real work:
Enemy->Player base values
image
Not relevant to this, but for any tank sims
image
https://www.engadget.com/2012-03-01-ghostcrawler-explains-stat-changes-in-mists-of-pandaria.html
image

@TheBackstabi
Copy link
Copy Markdown
Author

TheBackstabi commented May 14, 2025

I've left the negative Dodge/Parry/Block Base values for Enemy->Player as I assume those are needed for accurate mitigation calculations, even though an actual negative value should never happen.

Miss/Spell Miss I've left at 0 for 92/93 since there's no way of mitigating that anymore

I forgot ranged lmao

@TheBackstabi TheBackstabi requested a review from NerdEgghead May 14, 2025 00:53
Comment thread sim/core/spell_outcome.go Outdated
Comment on lines 503 to 504
if dot.Spell.Unit.PseudoStats.InFrontOfTarget {
if !result.applyAttackTableMissNoDWPenalty(dot.Spell, attackTable, roll, &chance) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to handle dodges here as well?

Copy link
Copy Markdown
Author

@TheBackstabi TheBackstabi May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snapshot initially made me think it was used in DoTs like Explosive Shot (Which can be tested to see how Dodges work once it's fixed), but looking closer I don't see anywhere it gets used

Comment thread sim/core/spell_outcome.go Outdated
Comment on lines +472 to +473
// TODO: Ranged Attacks are currently blockable on MoP Beta
// However, they shouldn't be! Revisit when fixed
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ranged attacks are also not currently dodge-able on Beta. Both are known issues that will likely be fixed, so we should handle them the same way: either make both Dodge and Block function like 5.4 in the sim under the assumption that both will be fixed, or make both function like Beta with a note to revisit both.

Comment thread sim/core/spell_outcome.go Outdated
Comment on lines +639 to +643
*chance += max(0, attackTable.BaseDodgeChance-spell.DodgeParrySuppression()-spell.Unit.PseudoStats.DodgeReduction)
*chance += max(0, attackTable.BaseDodgeChance-spell.DodgeSuppression()-spell.Unit.PseudoStats.DodgeReduction)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PseudoStats.DodgeReduction has been unused since WotLK, so I think you can delete it outright from the MoP codebase. Likewise, you should be able to delete the suppress_dodge field from proto.Target.

Comment thread sim/core/spell_outcome.go Outdated
Comment on lines 862 to 863
critChance := min(spell.PhysicalCritChance(attackTable), whiteCritCap)
averageMultiplier := 1.0 - missChance - dodgeChance - parryChance + (spell.CritDamageMultiplier()-1)*critChance - glanceChance*(1.0-attackTable.GlanceMultiplier) - blockChance*result.Target.BlockDamageReduction()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mathed it out, the correct formula for the MoP 2-roll white table should be:

averageMultiplier := (1.0 - missChance - dodgeChance - parryChance + (spell.CritDamageMultiplier()-1)*critChance - glanceChance*(1.0-attackTable.GlanceMultiplier)) * (1.0 - blockChance*result.Target.BlockDamageReduction())

Essentially, the average Block damage reduction from the second roll now gets multiplied across all the terms due to the possibility of Crit-Block and Glance-Block outcomes in MoP.

Comment thread sim/core/spell_outcome.go Outdated
Comment on lines 872 to 873
critChance := spell.PhysicalCritChance(attackTable)
averageMultiplier := (1.0 - missChance - dodgeChance - parryChance) * (1.0 + (spell.CritDamageMultiplier()-1)*critChance)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did the math for this one as well, in MoP the formula becomes:

critFactor := (spell.CritDamageMultiplier() - 1) * critChance
averageMultiplier := (1.0 - missChance - dodgeChance - parryChance) * (1.0 + critFactor - blockChance * (critFactor + result.Target.BlockDamageReduction()))

where you'd omit the -= term on the next line from the original formula.

The intuitive explanation is that it's the same formula from Cata, except that the "effective Block chance" now gets suppressed by the total miss chance for the ability, since misses now prevent Block from even being rolled unlike before.

Comment thread sim/core/spell_result.go
Comment thread sim/core/target.go Outdated
Comment on lines +264 to +258
table.BaseParryChance = UnitLevelFloat64(attacker.Level, 0, -0.002, -0.004, -0.006)
table.BaseSpellMissChance = UnitLevelFloat64(defender.Level, 0.06, 0.03, 0, 0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be attacker.Level here similar to the expressions below.

Comment thread sim/core/target.go Outdated
Comment on lines +259 to +262
table.BaseMissChance = UnitLevelFloat64(attacker.Level, 0.03, 0.015, 0, 0)
table.BaseBlockChance = UnitLevelFloat64(attacker.Level, 0.03, 0.015, 0, -0.015)
table.BaseDodgeChance = UnitLevelFloat64(attacker.Level, 0.03, 0.015, 0, -0.015)
table.BaseParryChance = UnitLevelFloat64(attacker.Level, 0.03, 0.015, 0, -0.015)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that +2 and +3 level enemies have a 0% chance to Miss with their autos? This seems odd, given that simc has specific logic coded to make auto-attack Miss outcomes not grant extra Vengeance and only refresh the existing Vengeance buff, unlike auto-attack Dodge and Parry outcomes which grant Vengeance as if they had landed. If boss auto Miss outcomes aren't even possible in the attack table, then they shouldn't have needed this branching logic.

Copy link
Copy Markdown
Author

@TheBackstabi TheBackstabi May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was also from the GC posts. I can do more log digging to double check, but so far everything that was said in that thread has held true when verified.

The mechanics of Vengeance were also mentioned - I think the case of a Miss is just mechanically present, but will never occur on a 92+.

It does hold relevance for trash mobs in Challenge Modes, if SimC had any form of their DungeonSlice model then

Copy link
Copy Markdown
Author

@TheBackstabi TheBackstabi May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some bosses appear to be flagged as Dual Wielding, and receive the very same +19% modifier to their miss chance. If we want to support that in an Encounter design, I will need to add in the negative values so the final miss rate is correct.

https://www.warcraftlogs.com/reports/aqYQNvtbV3L8CgmT?fight=3&type=damage-taken&source=22&ability=1&target=102
https://www.wowhead.com/mop-classic/npc=71479/he-softfoot

I've been otherwise unable to find any Miss hits from non-DW bosses (the rest of SoO, basically)

Comment thread sim/core/spell_outcome.go Outdated
Comment on lines 472 to 479
if spell.Unit.PseudoStats.InFrontOfTarget {
if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) {
if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) {
result.applyAttackTableBlock(spell, attackTable, roll, &chance)
} else {
if !result.applyAttackTableBlock(spell, attackTable, roll, &chance) {
result.applyAttackTableHit(spell, countHits)
}
}
if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) &&
!result.applyAttackTableDodge(spell, attackTable, roll, &chance) &&
!result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) {
result.applyAttackTableHit(spell, countHits)
}
} else {
if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) &&
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also need to handle the Dodge outcome in the case where the Hunter is attacking from behind. In fact, now that Blocks are no longer possible for ranged attacks, I don't think you even need to branch off InFrontOfTarget anymore - it should be the same attack table in both cases.

Comment thread sim/core/spell_outcome.go Outdated
Comment on lines 496 to 503
if dot.Spell.Unit.PseudoStats.InFrontOfTarget {
if !result.applyAttackTableMissNoDWPenalty(dot.Spell, attackTable, roll, &chance) {
if result.applyAttackTableCritSeparateRollSnapshot(sim, dot) {
result.applyAttackTableBlock(dot.Spell, attackTable, roll, &chance)
} else {
if !result.applyAttackTableBlock(dot.Spell, attackTable, roll, &chance) {
result.applyAttackTableHit(dot.Spell, countHits)
}
}
if !result.applyAttackTableMissNoDWPenalty(dot.Spell, attackTable, roll, &chance) &&
!result.applyAttackTableDodge(dot.Spell, attackTable, roll, &chance) &&
!result.applyAttackTableCritSeparateRollSnapshot(sim, dot) {
result.applyAttackTableHit(dot.Spell, countHits)
}
} else {
if !result.applyAttackTableMissNoDWPenalty(dot.Spell, attackTable, roll, &chance) &&
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, you're missing Dodge outcomes when attacking from behind, and shouldn't need to branch off InFrontOfTarget anymore in MoP if I'm understanding the changes correctly.

Comment thread sim/core/spell_outcome.go Outdated
Comment on lines 534 to 541
if spell.Unit.PseudoStats.InFrontOfTarget {
roll := sim.RandomFloat("White Hit Table")
chance := 0.0

if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) {
result.applyAttackTableBlock(spell, attackTable, roll, &chance)
result.applyAttackTableBlock(sim, spell, attackTable)
} else {
if !result.applyAttackTableBlock(spell, attackTable, roll, &chance) {
if !result.applyAttackTableBlock(sim, spell, attackTable) {
result.applyAttackTableHit(spell, countHits)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove ranged Block outcomes here for consistency if you're removing them everywhere else, which also means that InFrontOfTarget should no longer matter.

Comment thread sim/core/spell_outcome.go Outdated
Comment on lines 742 to 744
if sim.RandomFloat("Player Block") < chance {
result.Outcome |= OutcomeBlock
spell.SpellMetrics[result.Target.UnitIndex].Blocks++
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In MoP this also needs a branch for if result.DidCrit() like the player-vs-npc case, since CritBlock outcomes are now possible for enemy autos. It won't be relevant in practice since all tanks are Crit immune, but we might as well handle it correctly for completeness.

Copy link
Copy Markdown
Author

@TheBackstabi TheBackstabi May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should already be handled by the way outcomeEnemyMeleeWhite flows, in the exact same way Player->Target does. It checks Miss/Dodge/Parry, if none of those are true it then checks Crit. If it does Crit, then it rolls Block. If Block is true, it takes the Crit outcome and applies |= Block to the Outcome (as opposed to just setting it equal to, as the Crit/Miss/Dodge/Parry/Hit functions do)

Copy link
Copy Markdown
Author

@TheBackstabi TheBackstabi May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, to add to SpellMetrics for -specifically- CritBlock. I'll add this

@1337LutZ 1337LutZ merged commit 63cd7f0 into master May 19, 2025
2 checks passed
espionn pushed a commit to espionn/mop that referenced this pull request Mar 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants