From af9e9a983958e84229164f1e2329ace862c0703c Mon Sep 17 00:00:00 2001 From: FelixPflaum <141590183+FelixPflaum@users.noreply.github.com> Date: Sun, 17 Mar 2024 16:37:18 +0100 Subject: [PATCH 1/6] Fix physical multi school dot resistance --- sim/core/spell_resistances.go | 25 ++++++++++++++++--------- sim/core/spell_school_test.go | 30 ++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/sim/core/spell_resistances.go b/sim/core/spell_resistances.go index ab467c442d..788d0a2204 100644 --- a/sim/core/spell_resistances.go +++ b/sim/core/spell_resistances.go @@ -21,12 +21,12 @@ func (spell *Spell) ResistanceMultiplier(sim *Simulation, isPeriodic bool, attac } if spell.SpellSchool.Matches(SpellSchoolPhysical) { - // All physical dots (Bleeds) ignore armor. - if isPeriodic && !spell.Flags.Matches(SpellFlagApplyArmorReduction) { - return 1, OutcomeEmpty - } - if spell.SchoolIndex == stats.SchoolIndexPhysical || MultiSchoolShouldUseArmor(spell.SchoolIndex, attackTable.Defender) { + // All physical dots (Bleeds) ignore armor. + if isPeriodic && !spell.Flags.Matches(SpellFlagApplyArmorReduction) { + return 1, OutcomeEmpty + } + // Physical resistance (armor). return attackTable.GetArmorDamageModifier(spell), OutcomeEmpty } @@ -56,11 +56,18 @@ func (spell *Spell) ResistanceMultiplier(sim *Simulation, isPeriodic bool, attac } } -// Decide whether to use armor for physical multi school spells +// Decide whether to use armor for physical multi school spells. +// +// TODO: This is most likely not accurate for the case: armor near resistance but not 0 +// +// A short test showed that the game uses armor if it's far enough below resistance, +// but not simply if it's lower. +// 49 (and above) armor vs 57 res => used resistance +// 7 (and below) armor vs 57 res => used armor/no partials anymore +// If level based resist is used in this decission process is also not known as it was tested PvP. // -// TODO: This is most likely not accurate. A short test showed that it seems -// to not simply use armor if it's lower, but the breakpoint appeared to be pretty -// close to the resistance value. +// For most purposes this should work fine for now, but should be properly tested and fixed if +// spells using it become important and boss armor can actually go below (level based) resistance values. func MultiSchoolShouldUseArmor(schoolIndex stats.SchoolIndex, target *Unit) bool { resistance := 100000.0 lowestStat := stats.Armor diff --git a/sim/core/spell_school_test.go b/sim/core/spell_school_test.go index 1d7a9ac0de..8c94e52028 100644 --- a/sim/core/spell_school_test.go +++ b/sim/core/spell_school_test.go @@ -135,22 +135,36 @@ func Test_MultiSchoolResistanceArmor(t *testing.T) { Raid: &proto.Raid{}, }) + // Armor 100, resistances 0 => should use resistance defender.AddStat(stats.Armor, 100) - defender.AddStat(stats.FireResistance, 200) mult, outcome := spell.ResistanceMultiplier(sim, false, attackTable) + if outcome == OutcomeEmpty && mult < 1 { + t.Errorf("Expected partial or full hit with armor %f and resistance %f", defender.GetStat(stats.Armor), defender.GetStat(stats.FireResistance)) + return + } + + // Armor 100, resistances 300 => should use armor + defender.AddStat(stats.FireResistance, 300) + mult, outcome = spell.ResistanceMultiplier(sim, false, attackTable) if outcome != OutcomeEmpty || mult == 1 { - t.Errorf("Expected empty outcome with armor %f and resistance %f", defender.GetStat(stats.Armor), defender.GetStat(stats.FireResistance)) + t.Errorf("Expected empty outcome and mult < 1 with armor %f and resistance %f", defender.GetStat(stats.Armor), defender.GetStat(stats.FireResistance)) return } - defender.AddStat(stats.Armor, 200) - mult, outcome = spell.ResistanceMultiplier(sim, false, attackTable) - isPartial := (outcome & OutcomePartial) != OutcomePartial - isFullHit := outcome == OutcomeEmpty && mult == 1 - if !isFullHit && !isPartial { - t.Errorf("Expected empty outcome with armor %f and resistance %f", defender.GetStat(stats.Armor), defender.GetStat(stats.FireResistance)) + // Armor 400, resistances 300 => should use resistance (no non-partial hits) + defender.AddStat(stats.Armor, 300) + + _, outcome = spell.ResistanceMultiplier(sim, false, attackTable) + if (outcome & OutcomePartial) == 0 { + t.Errorf("Expected partial hit with armor %f and resistance %f", defender.GetStat(stats.Armor), defender.GetStat(stats.FireResistance)) + return + } + + _, outcome = spell.ResistanceMultiplier(sim, true, attackTable) + if (outcome & OutcomePartial) == 0 { + t.Errorf("Expected partial hit for periodic with armor %f and resistance %f", defender.GetStat(stats.Armor), defender.GetStat(stats.FireResistance)) return } } From c37204084083f1419605b60d032050c72eaa0960 Mon Sep 17 00:00:00 2001 From: FelixPflaum <141590183+FelixPflaum@users.noreply.github.com> Date: Sun, 17 Mar 2024 18:45:04 +0100 Subject: [PATCH 2/6] refactor wip --- sim/core/spell.go | 13 +- sim/core/spell_resistances.go | 83 ++++---- sim/core/spell_resistances_test.go | 31 +-- sim/core/spell_result.go | 2 +- sim/core/spell_school.go | 146 +++----------- sim/core/spell_school_test.go | 309 +++++++++++++++++------------ sim/core/stats/stats.go | 56 +----- 7 files changed, 302 insertions(+), 338 deletions(-) diff --git a/sim/core/spell.go b/sim/core/spell.go index 3d0720ffdf..b32b414100 100644 --- a/sim/core/spell.go +++ b/sim/core/spell.go @@ -73,10 +73,9 @@ type Spell struct { // The unit who will perform this spell. Unit *Unit - SpellSchool SpellSchool // Schoolmask of all schools this spell uses. Use Spell.SetSchool() to change this! - SchoolIndex stats.SchoolIndex // Use Spell.SetSchool() to change this! - SchoolBaseIndices []stats.SchoolIndex // Base school indices for multi schools. Use Spell.SetSchool() to change this! - IsMultischool bool // True if school is composed of multiple base schools. Use Spell.SetSchool() to change this! + SpellSchool SpellSchool // Schoolmask of all schools this spell uses. Do not change this! Whatever you try to do is a hack and probably wrong. + SchoolIndex stats.SchoolIndex // Do not change this! Whatever you try to do is a hack and probably wrong. + SchoolBaseIndices []stats.SchoolIndex // Base school indices for multi schools. Do not change this! Whatever you try to do is a hack and probably wrong. // Controls which effects can proc from this spell. ProcMask ProcMask @@ -217,6 +216,10 @@ func (unit *Unit) RegisterSpell(config SpellConfig) *Spell { CastType: config.CastType, MissileSpeed: config.MissileSpeed, + SpellSchool: config.SpellSchool, + SchoolIndex: config.SpellSchool.GetSchoolIndex(), + SchoolBaseIndices: config.SpellSchool.GetBaseIndices(), + DefaultCast: config.Cast.DefaultCast, CD: config.Cast.CD, SharedCD: config.Cast.SharedCD, @@ -248,8 +251,6 @@ func (unit *Unit) RegisterSpell(config SpellConfig) *Spell { RelatedAuras: config.RelatedAuras, } - spell.SetSchool(config.SpellSchool.GetSchoolIndex()) - spell.CdSpell = spell // newXXXCost() all update spell.DefaultCast.Cost diff --git a/sim/core/spell_resistances.go b/sim/core/spell_resistances.go index 788d0a2204..75fd46a4a4 100644 --- a/sim/core/spell_resistances.go +++ b/sim/core/spell_resistances.go @@ -21,7 +21,7 @@ func (spell *Spell) ResistanceMultiplier(sim *Simulation, isPeriodic bool, attac } if spell.SpellSchool.Matches(SpellSchoolPhysical) { - if spell.SchoolIndex == stats.SchoolIndexPhysical || MultiSchoolShouldUseArmor(spell.SchoolIndex, attackTable.Defender) { + if spell.SchoolIndex == stats.SchoolIndexPhysical || MultiSchoolShouldUseArmor(spell, attackTable.Defender) { // All physical dots (Bleeds) ignore armor. if isPeriodic && !spell.Flags.Matches(SpellFlagApplyArmorReduction) { return 1, OutcomeEmpty @@ -39,7 +39,7 @@ func (spell *Spell) ResistanceMultiplier(sim *Simulation, isPeriodic bool, attac resistanceRoll := sim.RandomFloat("Partial Resist") - threshold00, threshold25, threshold50 := attackTable.GetPartialResistThresholds(spell.SchoolIndex, spell.Flags.Matches(SpellFlagPureDot)) + threshold00, threshold25, threshold50 := attackTable.GetPartialResistThresholds(spell, spell.Flags.Matches(SpellFlagPureDot)) //if sim.Log != nil { // sim.Log("Resist thresholds: %0.04f, %0.04f, %0.04f", threshold00, threshold25, threshold50) //} @@ -68,17 +68,17 @@ func (spell *Spell) ResistanceMultiplier(sim *Simulation, isPeriodic bool, attac // // For most purposes this should work fine for now, but should be properly tested and fixed if // spells using it become important and boss armor can actually go below (level based) resistance values. -func MultiSchoolShouldUseArmor(schoolIndex stats.SchoolIndex, target *Unit) bool { +func MultiSchoolShouldUseArmor(spell *Spell, target *Unit) bool { resistance := 100000.0 - lowestStat := stats.Armor - for _, resiStat := range GetSchoolResistanceStats(schoolIndex) { - resiVal := target.GetStat(resiStat) + lowestIsArmor := true + for _, baseSchoolIndex := range spell.SchoolBaseIndices { + resiVal := target.GetResistanceForSchool(baseSchoolIndex) if resiVal < resistance { resistance = resiVal - lowestStat = resiStat + lowestIsArmor = baseSchoolIndex == stats.SchoolIndexPhysical } } - return lowestStat == stats.Armor + return lowestIsArmor } func (at *AttackTable) GetArmorDamageModifier(spell *Spell) float64 { @@ -87,50 +87,59 @@ func (at *AttackTable) GetArmorDamageModifier(spell *Spell) float64 { return 1 - defenderArmor/(defenderArmor+400+85*float64(at.Attacker.Level)) } -func (at *AttackTable) GetPartialResistThresholds(si stats.SchoolIndex, pureDot bool) (float64, float64, float64) { - return at.Defender.partialResistRollThresholds(si, at.Attacker, pureDot) +func (at *AttackTable) GetPartialResistThresholds(spell *Spell, pureDot bool) (float64, float64, float64) { + return at.Defender.partialResistRollThresholds(spell, at.Attacker, pureDot) } -func (at *AttackTable) GetBinaryHitChance(si stats.SchoolIndex) float64 { - return at.Defender.binaryHitChance(si, at.Attacker) +func (at *AttackTable) GetBinaryHitChance(spell *Spell) float64 { + return at.Defender.binaryHitChance(spell, at.Attacker) } -// All of the following calculations are based on this guide: -// https://royalgiraffe.github.io/resist-guide - -func (unit *Unit) resistCoeff(schoolIndex stats.SchoolIndex, attacker *Unit, binary bool, pureDot bool) float64 { - var resistance float64 - +// Only for base schools! +func (unit *Unit) GetResistanceForSchool(schoolIndex stats.SchoolIndex) float64 { switch schoolIndex { case stats.SchoolIndexNone: return 0 case stats.SchoolIndexPhysical: - return 0 + return unit.GetStat(stats.Armor) case stats.SchoolIndexArcane: - resistance = unit.GetStat(stats.ArcaneResistance) + return unit.GetStat(stats.ArcaneResistance) case stats.SchoolIndexFire: - resistance = unit.GetStat(stats.FireResistance) + return unit.GetStat(stats.FireResistance) case stats.SchoolIndexFrost: - resistance = unit.GetStat(stats.FrostResistance) + return unit.GetStat(stats.FrostResistance) case stats.SchoolIndexHoly: - resistance = 0 // Holy resistance doesn't exist. + return 0 // Holy resistance doesn't exist. case stats.SchoolIndexNature: - resistance = unit.GetStat(stats.NatureResistance) + return unit.GetStat(stats.NatureResistance) case stats.SchoolIndexShadow: - resistance = unit.GetStat(stats.ShadowResistance) + return unit.GetStat(stats.ShadowResistance) default: + return 0 + } +} + +// All of the following calculations are based on this guide: +// https://royalgiraffe.github.io/resist-guide + +func (unit *Unit) resistCoeff(spell *Spell, attacker *Unit, binary bool, pureDot bool) float64 { + if spell.SchoolIndex <= stats.SchoolIndexPhysical { + return 0 + } + + var resistance float64 + + if spell.SchoolIndex.IsMultiSchool() { // Multi school: Choose lowest resistance available. resistance = 1000.0 - if !SpellSchoolFromIndex(schoolIndex).Matches(SpellSchoolHoly) { - for _, resiStat := range GetSchoolResistanceStats(schoolIndex) { - resiVal := unit.GetStat(resiStat) - if resiVal < resistance { - resistance = resiVal - } + for _, baseSchoolIndex := range spell.SchoolBaseIndices { + resiVal := unit.GetResistanceForSchool(baseSchoolIndex) + if resiVal < resistance { + resistance = resiVal } - } else { - resistance = 0.0 } + } else { + resistance = unit.GetResistanceForSchool(spell.SchoolIndex) } resistance = max(0, resistance-attacker.stats[stats.SpellPenetration]) @@ -153,14 +162,14 @@ func (unit *Unit) resistCoeff(schoolIndex stats.SchoolIndex, attacker *Unit, bin return min(1, resistanceCoef) } -func (unit *Unit) binaryHitChance(schoolIndex stats.SchoolIndex, attacker *Unit) float64 { - resistCoeff := unit.resistCoeff(schoolIndex, attacker, true, false) +func (unit *Unit) binaryHitChance(spell *Spell, attacker *Unit) float64 { + resistCoeff := unit.resistCoeff(spell, attacker, true, false) return 1 - 0.75*resistCoeff } // Roll threshold for each type of partial resist. -func (unit *Unit) partialResistRollThresholds(schoolIndex stats.SchoolIndex, attacker *Unit, pureDot bool) (float64, float64, float64) { - resistCoeff := unit.resistCoeff(schoolIndex, attacker, false, pureDot) +func (unit *Unit) partialResistRollThresholds(spell *Spell, attacker *Unit, pureDot bool) (float64, float64, float64) { + resistCoeff := unit.resistCoeff(spell, attacker, false, pureDot) // Based on the piecewise linear regression estimates at https://royalgiraffe.github.io/partial-resist-table. //partialResistChance00 := piecewiseLinear3(resistCoeff, 1, 0.24, 0.00, 0.00) diff --git a/sim/core/spell_resistances_test.go b/sim/core/spell_resistances_test.go index 4d905aeeb9..1b7c15f715 100644 --- a/sim/core/spell_resistances_test.go +++ b/sim/core/spell_resistances_test.go @@ -35,7 +35,7 @@ func Test_PartialResistsVsPlayer(t *testing.T) { for resist := 0; resist < 5_000; resist += 1 { defender.stats[stats.FireResistance] = float64(resist) - threshold00, threshold25, threshold50 := attackTable.Defender.partialResistRollThresholds(stats.SchoolIndexFire, attackTable.Attacker, false) + threshold00, threshold25, threshold50 := attackTable.Defender.partialResistRollThresholds(spell, attackTable.Attacker, false) thresholds := [4]float64{threshold00, threshold25, threshold50, 0.0} var cumulativeChance float64 @@ -49,7 +49,7 @@ func Test_PartialResistsVsPlayer(t *testing.T) { } } - resistanceScore := attackTable.Defender.resistCoeff(stats.SchoolIndexFire, attackTable.Attacker, false, false) + resistanceScore := attackTable.Defender.resistCoeff(spell, attackTable.Attacker, false, false) expectedAr := 0.75*resistanceScore - 3.0/16.0*max(0.0, resistanceScore-2.0/3.0) if math.Abs(resultingAr-expectedAr) > 1e-2 { @@ -106,8 +106,12 @@ func ResistanceCheck(t *testing.T, isDoT bool) { attackTable := NewAttackTable(attacker, defender, nil) - spell := &Spell{} - spell.SetSchool(stats.SchoolIndexNature) + schoolMask := SpellSchoolFromIndex(stats.SchoolIndexNature) + spell := &Spell{ + SpellSchool: schoolMask, + SchoolIndex: schoolMask.GetSchoolIndex(), + SchoolBaseIndices: schoolMask.GetBaseIndices(), + } if isDoT { spell.Flags |= SpellFlagPureDot @@ -117,7 +121,7 @@ func ResistanceCheck(t *testing.T, isDoT bool) { // Check if coef is 0.08 (from +3 level based resist) at 0 res defender.stats[stats.NatureResistance] = 0 - coef := defender.resistCoeff(spell.SchoolIndex, attacker, false, isDoT) + coef := defender.resistCoeff(spell, attacker, false, isDoT) if coef != 0.08 { t.Errorf("Resist coef is %.3f at 0 resistance, but should be 0.08!", coef) return @@ -131,7 +135,7 @@ func ResistanceCheck(t *testing.T, isDoT bool) { expectedMitigation = 0.11 expectedChances = []float64{0.67, 0.24, 0.08, 0.01} } - threshold00, threshold25, threshold50 := attackTable.GetPartialResistThresholds(spell.SchoolIndex, spell.Flags.Matches(SpellFlagPureDot)) + threshold00, threshold25, threshold50 := attackTable.GetPartialResistThresholds(spell, spell.Flags.Matches(SpellFlagPureDot)) avgResist, chance0, chance25, chance50, chance75 := GetChancesAndMitFromThresholds(threshold00, threshold25, threshold50) if !CloseEnough(avgResist, expectedMitigation, 0.01) { t.Errorf("Avg mitigation %.3f at 200 resistance, but should be %.3f!", avgResist, expectedMitigation) @@ -161,14 +165,14 @@ func ResistanceCheck(t *testing.T, isDoT bool) { expectedAvgMitigation := expectedCoef*0.75 - 3.0/16.0*max(0, expectedCoef-2.0/3.0) // Check if coef is correct to begin with - resistCoef := defender.resistCoeff(spell.SchoolIndex, attacker, false, isDoT) + resistCoef := defender.resistCoeff(spell, attacker, false, isDoT) if math.Abs(resistCoef-expectedCoef) > 0.001 { t.Errorf("Resist coef is %.3f but expected %.3f at resistance %f", resistCoef, expectedCoef, resistanceUsed) return } // Check breakpoints - threshold00, threshold25, threshold50 := attackTable.GetPartialResistThresholds(spell.SchoolIndex, spell.Flags.Matches(SpellFlagPureDot)) + threshold00, threshold25, threshold50 := attackTable.GetPartialResistThresholds(spell, spell.Flags.Matches(SpellFlagPureDot)) avgResist, _, _, _, _ := GetChancesAndMitFromThresholds(threshold00, threshold25, threshold50) if math.Abs(avgResist-expectedAvgMitigation) > 0.005 { t.Errorf("resist = %.2f, thresholds = %f, resultingAr = %.2f%%, expectedAr = %.2f%%", resistanceUsed, threshold00, avgResist, expectedAvgMitigation) @@ -196,14 +200,17 @@ func Test_ResistBinary(t *testing.T) { attackTable := NewAttackTable(attacker, defender, nil) + schoolMask := SpellSchoolFromIndex(stats.SchoolIndexNature) spell := &Spell{ - Flags: SpellFlagBinary, + Flags: SpellFlagBinary, + SpellSchool: schoolMask, + SchoolIndex: schoolMask.GetSchoolIndex(), + SchoolBaseIndices: schoolMask.GetBaseIndices(), } - spell.SetSchool(stats.SchoolIndexNature) // Check if coef is 0.0 at 0 resistance, binary spells do not get level based resistance! defender.stats[stats.NatureResistance] = 0 - coef := defender.resistCoeff(spell.SchoolIndex, attacker, true, false) + coef := defender.resistCoeff(spell, attacker, true, false) if coef != 0.0 { t.Errorf("Resist coef is %.3f at 0 resistance for binary spell, but should be 0.0!", coef) return @@ -227,7 +234,7 @@ func Test_ResistBinary(t *testing.T) { resistance := test[0] defender.stats[stats.NatureResistance] = resistance expectedResult := test[1] - result := attackTable.GetBinaryHitChance(spell.SchoolIndex) + result := attackTable.GetBinaryHitChance(spell) if !CloseEnough(result, expectedResult, 0.000001) { t.Errorf("Binary hit chance result at %.0f resistance was %.3f, expected %.3f!", resistance, result, expectedResult) return diff --git a/sim/core/spell_result.go b/sim/core/spell_result.go index 4a728b5a80..552894cb32 100644 --- a/sim/core/spell_result.go +++ b/sim/core/spell_result.go @@ -178,7 +178,7 @@ func (spell *Spell) SpellChanceToMiss(attackTable *AttackTable) float64 { missChance := 0.01 if spell.Flags.Matches(SpellFlagBinary) { - baseHitChance := (1 - attackTable.BaseSpellMissChance) * attackTable.GetBinaryHitChance(spell.SchoolIndex) + baseHitChance := (1 - attackTable.BaseSpellMissChance) * attackTable.GetBinaryHitChance(spell) missChance = 1 - baseHitChance - spell.SpellHitChance(attackTable.Defender) } else { missChance = attackTable.BaseSpellMissChance - spell.SpellHitChance(attackTable.Defender) diff --git a/sim/core/spell_school.go b/sim/core/spell_school.go index 6ad1f67f5b..0c56b4fdfc 100644 --- a/sim/core/spell_school.go +++ b/sim/core/spell_school.go @@ -1,8 +1,6 @@ package core import ( - "fmt" - "github.com/wowsims/sod/sim/core/proto" "github.com/wowsims/sod/sim/core/stats" ) @@ -71,97 +69,8 @@ var schoolIndexToSchoolMask = [stats.SchoolLen]SpellSchool{ SpellSchoolHoly, SpellSchoolNature, SpellSchoolShadow, - - // Physical x Other - SpellSchoolSpellstrike, - SpellSchoolFlamestrike, - SpellSchoolFroststrike, - SpellSchoolHolystrike, - SpellSchoolStormstrike, - SpellSchoolShadowstrike, - - // Arcane x Other - SpellSchoolSpellfire, - SpellSchoolSpellFrost, - SpellSchoolDivine, - SpellSchoolAstral, - SpellSchoolSpellShadow, - - // Fire x Other - SpellSchoolFrostfire, - SpellSchoolRadiant, - SpellSchoolVolcanic, - SpellSchoolShadowflame, - - // Frost x Other - SpellSchoolHolyfrost, - SpellSchoolFroststorm, - SpellSchoolShadowfrost, - - // Holy x Other - SpellSchoolHolystorm, - SpellSchoolTwilight, - - // Nature x Other - SpellSchoolPlague, - - SpellSchoolElemental, } -var schoolMaskToIndex = func() map[SpellSchool]stats.SchoolIndex { - mti := map[SpellSchool]stats.SchoolIndex{} - for i := stats.SchoolIndexNone; i < stats.SchoolLen; i++ { - mti[schoolIndexToSchoolMask[i]] = i - } - return mti -}() - -// LUT for base school indices a (multi)school is made of. -var schoolIndexToIndices = func() [stats.SchoolLen][]stats.SchoolIndex { - arr := [stats.SchoolLen][]stats.SchoolIndex{} - - for schoolIndex := stats.SchoolIndexNone; schoolIndex < stats.SchoolLen; schoolIndex++ { - multiMask := SpellSchoolFromIndex(schoolIndex) - indexArr := []stats.SchoolIndex{} - for baseSchoolIndex := stats.SchoolIndexNone; baseSchoolIndex < stats.PrimarySchoolLen; baseSchoolIndex++ { - schoolFlag := SpellSchoolFromIndex(baseSchoolIndex) - if multiMask.Matches(schoolFlag) { - indexArr = append(indexArr, baseSchoolIndex) - } - } - arr[schoolIndex] = indexArr - } - - return arr -}() - -// LUT for resistance stat indices used by each multischool. -var schoolIndexToResistanceStats = func() [stats.SchoolLen][]stats.Stat { - resistances := map[SpellSchool]stats.Stat{ - SpellSchoolPhysical: stats.Armor, // This is technically physical resistance - SpellSchoolArcane: stats.ArcaneResistance, - SpellSchoolFire: stats.FireResistance, - SpellSchoolFrost: stats.FrostResistance, - SpellSchoolNature: stats.NatureResistance, - SpellSchoolShadow: stats.ShadowResistance, - } - - arr := [stats.SchoolLen][]stats.Stat{} - - for schoolIndex := stats.SchoolIndexPhysical; schoolIndex < stats.SchoolLen; schoolIndex++ { - schoolMask := SpellSchoolFromIndex(schoolIndex) - resiArr := []stats.Stat{} - for resiSchool, resiStat := range resistances { - if schoolMask.Matches(resiSchool) { - resiArr = append(resiArr, resiStat) - } - } - arr[schoolIndex] = resiArr - } - - return arr -}() - // Get spell school mask from school index. func SpellSchoolFromIndex(schoolIndex stats.SchoolIndex) SpellSchool { return schoolIndexToSchoolMask[schoolIndex] @@ -188,36 +97,47 @@ func SpellSchoolFromProto(p proto.SpellSchool) SpellSchool { } } -// Get array of resistance stat indices for a (multi)school. -// Physical school uses Armor as stat index! -func GetSchoolResistanceStats(schoolIndex stats.SchoolIndex) []stats.Stat { - return schoolIndexToResistanceStats[schoolIndex] -} - // Returns whether there is any overlap between the given masks. func (ss SpellSchool) Matches(other SpellSchool) bool { return (ss & other) != 0 } -// Get school index from school mask. Will error if mask is for an undefined multi-school. -// This involves a map lookup. Do not use in hot path code. +// Get school index from school mask. func (ss SpellSchool) GetSchoolIndex() stats.SchoolIndex { - idx, ok := schoolMaskToIndex[ss] - if !ok { - panic(fmt.Sprintf("No school index defined for schoolmask %d! You may need to define a new multi-school.", ss)) + switch ss { + case SpellSchoolNone: + return stats.SchoolIndexNone + case SpellSchoolPhysical: + return stats.SchoolIndexPhysical + case SpellSchoolArcane: + return stats.SchoolIndexArcane + case SpellSchoolFire: + return stats.SchoolIndexFire + case SpellSchoolFrost: + return stats.SchoolIndexFrost + case SpellSchoolHoly: + return stats.SchoolIndexHoly + case SpellSchoolNature: + return stats.SchoolIndexNature + case SpellSchoolShadow: + return stats.SchoolIndexShadow + default: + return stats.SchoolIndexMultischool } - return idx } -// Sets school index, mask and base indices. -func (spell *Spell) SetSchool(schoolIndex stats.SchoolIndex) { - spell.SchoolIndex = schoolIndex - spell.SpellSchool = SpellSchoolFromIndex(schoolIndex) - spell.SchoolBaseIndices = schoolIndexToIndices[schoolIndex] - spell.IsMultischool = schoolIndex.IsMultiSchool() +func (schoolMask SpellSchool) GetBaseIndices() []stats.SchoolIndex { + indexArr := []stats.SchoolIndex{} + for baseSchoolIndex := stats.SchoolIndexNone; baseSchoolIndex < stats.SchoolLen; baseSchoolIndex++ { + schoolFlag := SpellSchoolFromIndex(baseSchoolIndex) + if schoolMask.Matches(schoolFlag) { + indexArr = append(indexArr, baseSchoolIndex) + } + } + return indexArr } -func selectMaxMultInSchoolArray(spell *Spell, array *[stats.PrimarySchoolLen]float64) float64 { +func selectMaxMultInSchoolArray(spell *Spell, array *[stats.SchoolLen]float64) float64 { high := 0.0 for _, baseIndex := range spell.SchoolBaseIndices { mult := array[baseIndex] @@ -231,7 +151,7 @@ func selectMaxMultInSchoolArray(spell *Spell, array *[stats.PrimarySchoolLen]flo // Get school damage done multiplier. // Returns highest multiplier if spell is multi school. func (unit *Unit) GetSchoolDamageDoneMultiplier(spell *Spell) float64 { - if !spell.IsMultischool { + if !spell.SchoolIndex.IsMultiSchool() { return unit.PseudoStats.SchoolDamageDealtMultiplier[spell.SchoolIndex] } return selectMaxMultInSchoolArray(spell, &unit.PseudoStats.SchoolDamageDealtMultiplier) @@ -240,7 +160,7 @@ func (unit *Unit) GetSchoolDamageDoneMultiplier(spell *Spell) float64 { // Get school damage taken multiplier. // Returns highest multiplier if spell is multi school. func (unit *Unit) GetSchoolDamageTakenMultiplier(spell *Spell) float64 { - if !spell.IsMultischool { + if !spell.SchoolIndex.IsMultiSchool() { return unit.PseudoStats.SchoolDamageTakenMultiplier[spell.SchoolIndex] } return selectMaxMultInSchoolArray(spell, &unit.PseudoStats.SchoolDamageTakenMultiplier) @@ -249,7 +169,7 @@ func (unit *Unit) GetSchoolDamageTakenMultiplier(spell *Spell) float64 { // Get school crit taken multiplier. // Returns highest multiplier if spell is multi school. func (unit *Unit) GetCritTakenMultiplier(spell *Spell) float64 { - if !spell.IsMultischool { + if !spell.SchoolIndex.IsMultiSchool() { return unit.PseudoStats.SchoolCritTakenMultiplier[spell.SchoolIndex] } return selectMaxMultInSchoolArray(spell, &unit.PseudoStats.SchoolCritTakenMultiplier) diff --git a/sim/core/spell_school_test.go b/sim/core/spell_school_test.go index 8c94e52028..5876fb2b28 100644 --- a/sim/core/spell_school_test.go +++ b/sim/core/spell_school_test.go @@ -19,6 +19,25 @@ func Test_MultiSchoolIndexMapping(t *testing.T) { } } +func getResiStatForSchool(schoolIndex stats.SchoolIndex) stats.Stat { + switch schoolIndex { + case stats.SchoolIndexPhysical: + return stats.Armor + case stats.SchoolIndexArcane: + return stats.ArcaneResistance + case stats.SchoolIndexFire: + return stats.FireResistance + case stats.SchoolIndexFrost: + return stats.FrostResistance + case stats.SchoolIndexNature: + return stats.NatureResistance + case stats.SchoolIndexShadow: + return stats.ShadowResistance + default: + return stats.Armor + } +} + func Test_MultiSchoolResistance(t *testing.T) { attacker := &Unit{ Type: PlayerUnit, @@ -34,85 +53,109 @@ func Test_MultiSchoolResistance(t *testing.T) { attackTable := NewAttackTable(attacker, defender, nil) - for schoolIndex := stats.SchoolIndexArcane; schoolIndex < stats.SchoolLen; schoolIndex++ { - spell := &Spell{} - spell.SetSchool(schoolIndex) - - resistanceStats := GetSchoolResistanceStats(spell.SchoolIndex) - const lowestValue float64 = 50.0 - var lowestStat stats.Stat - isHoly := false + spell := &Spell{} - for rev := 0; rev < 2; rev++ { - if spell.SpellSchool.Matches(SpellSchoolHoly) { - isHoly = true - } else { - if rev != 0 { - resiLen := len(resistanceStats) - lowestStat = resistanceStats[resiLen-1] - j := resiLen - 1 - for i := 0; i < resiLen; i++ { - defender.stats[resistanceStats[j]] = lowestValue + 25.0*float64(i) - j-- - } - } else { - lowestStat = resistanceStats[0] - for i, stat := range resistanceStats { - defender.stats[stat] = lowestValue + 25.0*float64(i) - } - } + for schoolIndex1 := stats.SchoolIndexArcane; schoolIndex1 < stats.SchoolLen; schoolIndex1++ { + for schoolIndex2 := stats.SchoolIndexArcane; schoolIndex2 < stats.SchoolLen; schoolIndex2++ { + if schoolIndex2 == schoolIndex1 { + continue } - resistance := 0.0 + schoolMask := SpellSchoolFromIndex(schoolIndex1) | SpellSchoolFromIndex(schoolIndex2) + spell.SpellSchool = schoolMask + spell.SchoolIndex = schoolMask.GetSchoolIndex() + spell.SchoolBaseIndices = schoolMask.GetBaseIndices() - if !isHoly { - resistance = lowestValue + baseIndices := spell.SchoolBaseIndices + const lowestValue float64 = 50.0 + var lowestSchool stats.SchoolIndex + isHoly := false - // Make sure setup is right - var lowestFound stats.Stat - lowestValFound := 99999.0 - for _, checkStat := range resistanceStats { - if defender.GetStat(checkStat) < lowestValFound { - lowestValFound = defender.GetStat(checkStat) - lowestFound = checkStat + for rev := 0; rev < 2; rev++ { + if spell.SpellSchool.Matches(SpellSchoolHoly) { + isHoly = true + } else { + if rev != 0 { + indicesLen := len(baseIndices) + lowestSchool = baseIndices[indicesLen-1] + j := indicesLen - 1 + for i := 0; i < indicesLen; i++ { + if baseIndices[j] != stats.SchoolIndexHoly { + defender.stats[getResiStatForSchool(baseIndices[j])] = lowestValue + 25.0*float64(i) + } + j-- + } + } else { + lowestSchool = baseIndices[0] + for i, baseIndex := range baseIndices { + if baseIndex != stats.SchoolIndexHoly { + defender.stats[getResiStatForSchool(baseIndex)] = lowestValue + 25.0*float64(i) + } + } } } - if lowestFound != lowestStat || lowestValFound != resistance { - t.Errorf("Expected resist %d to be lowest with %f, but found %d at %f to be lowest resist!", lowestStat, resistance, lowestFound, lowestValFound) - return + + resistance := 0.0 + + if !isHoly { + resistance = lowestValue + + // Make sure setup is right + var lowestFound stats.SchoolIndex + lowestValFound := 99999.0 + for _, checkIndex := range baseIndices { + if checkIndex == stats.SchoolIndexHoly { + lowestFound = checkIndex + lowestValFound = 0 + } else { + stat := getResiStatForSchool(checkIndex) + if defender.GetStat(stat) < lowestValFound { + lowestValFound = defender.GetStat(stat) + lowestFound = checkIndex + } + } + } + if lowestFound != lowestSchool || lowestValFound != resistance { + t.Errorf("Expected resist %d to be lowest with %f, but found %d at %f to be lowest resist!", lowestSchool, resistance, lowestFound, lowestValFound) + return + } } - } - // Expected values - resistanceCap := float64(attacker.Level * 5) - levelBased := float64(max(defender.Level-attacker.Level, 0)) * 0.02 - expectedCoef := min(1, resistance/resistanceCap+levelBased*1/0.75) - expectedAvgMitigation := expectedCoef*0.75 - 3.0/16.0*max(0, expectedCoef-2.0/3.0) + // Expected values + resistanceCap := float64(attacker.Level * 5) + levelBased := float64(max(defender.Level-attacker.Level, 0)) * 0.02 + expectedCoef := min(1, resistance/resistanceCap+levelBased*1/0.75) + expectedAvgMitigation := expectedCoef*0.75 - 3.0/16.0*max(0, expectedCoef-2.0/3.0) - // Check if coef is correct to begin with - resistCoef := defender.resistCoeff(spell.SchoolIndex, attacker, false, false) - if math.Abs(resistCoef-expectedCoef) > 0.001 { - t.Errorf("Resist coef is %.3f but expected %.3f at resistance %f", resistCoef, expectedCoef, resistance) - return - } + // Check if coef is correct to begin with + resistCoef := defender.resistCoeff(spell, attacker, false, false) + if math.Abs(resistCoef-expectedCoef) > 0.001 { + t.Errorf("Resist coef is %.3f but expected %.3f at resistance %f", resistCoef, expectedCoef, resistance) + return + } - // Check breakpoints - threshold00, threshold25, threshold50 := attackTable.GetPartialResistThresholds(spell.SchoolIndex, spell.Flags.Matches(SpellFlagPureDot)) - chance25 := threshold00 - threshold25 - chance50 := threshold25 - threshold50 - chance75 := threshold50 - avgResist := chance25*0.25 + chance50*0.50 + chance75*0.75 - if math.Abs(avgResist-expectedAvgMitigation) > 0.005 { - t.Errorf("resist = %.2f, thresholds = %f, resultingAr = %.2f%%, expectedAr = %.2f%%", resistance, threshold00, avgResist, expectedAvgMitigation) - return + // Check breakpoints + threshold00, threshold25, threshold50 := attackTable.GetPartialResistThresholds(spell, spell.Flags.Matches(SpellFlagPureDot)) + chance25 := threshold00 - threshold25 + chance50 := threshold25 - threshold50 + chance75 := threshold50 + avgResist := chance25*0.25 + chance50*0.50 + chance75*0.75 + if math.Abs(avgResist-expectedAvgMitigation) > 0.005 { + t.Errorf("resist = %.2f, thresholds = %f, resultingAr = %.2f%%, expectedAr = %.2f%%", resistance, threshold00, avgResist, expectedAvgMitigation) + return + } } } } } func Test_MultiSchoolResistanceArmor(t *testing.T) { - spell := &Spell{} - spell.SetSchool(stats.SchoolIndexFlamestrike) + ss := SpellSchoolFlamestrike + spell := &Spell{ + SpellSchool: ss, + SchoolIndex: ss.GetSchoolIndex(), + SchoolBaseIndices: ss.GetBaseIndices(), + } attacker := &Unit{ Type: PlayerUnit, @@ -181,66 +224,75 @@ func Test_MultiSchoolSpellPower(t *testing.T) { Unit: caster, } - for schoolIndex := stats.SchoolIndexArcane; schoolIndex < stats.SchoolLen; schoolIndex++ { - spell.SetSchool(schoolIndex) + for schoolIndex1 := stats.SchoolIndexArcane; schoolIndex1 < stats.SchoolLen; schoolIndex1++ { + for schoolIndex2 := stats.SchoolIndexArcane; schoolIndex2 < stats.SchoolLen; schoolIndex2++ { + if schoolIndex2 == schoolIndex1 { + continue + } - baseIndices := spell.SchoolBaseIndices - const highestValue float64 = 555.0 - var highestStat stats.Stat + schoolMask := SpellSchoolFromIndex(schoolIndex1) | SpellSchoolFromIndex(schoolIndex2) + spell.SpellSchool = schoolMask + spell.SchoolIndex = schoolMask.GetSchoolIndex() + spell.SchoolBaseIndices = schoolMask.GetBaseIndices() - // Note: If powerStat == stats.SpellPower, then that means physical school, which is PseudoStats.BonusDamage! + baseIndices := spell.SchoolBaseIndices + const highestValue float64 = 555.0 + var highestStat stats.Stat - for rev := 0; rev < 2; rev++ { - if rev != 0 { - indexLen := len(baseIndices) - highestStat = stats.ArcanePower + stats.Stat(baseIndices[indexLen-1]) - 2 + // Note: If powerStat == stats.SpellPower, then that means physical school, which is PseudoStats.BonusDamage! - for i := indexLen - 1; i >= 0; i-- { - powerStat := stats.ArcanePower + stats.Stat(baseIndices[i]) - 2 - if powerStat == stats.SpellPower { - caster.PseudoStats.BonusDamage = highestValue - 25.0*float64(indexLen-1-i) - } else { - caster.stats[powerStat] = highestValue - 25.0*float64(indexLen-1-i) + for rev := 0; rev < 2; rev++ { + if rev != 0 { + indexLen := len(baseIndices) + highestStat = stats.ArcanePower + stats.Stat(baseIndices[indexLen-1]) - 2 + + for i := indexLen - 1; i >= 0; i-- { + powerStat := stats.ArcanePower + stats.Stat(baseIndices[i]) - 2 + if powerStat == stats.SpellPower { + caster.PseudoStats.BonusDamage = highestValue - 25.0*float64(indexLen-1-i) + } else { + caster.stats[powerStat] = highestValue - 25.0*float64(indexLen-1-i) + } + } + } else { + highestStat = stats.ArcanePower + stats.Stat(baseIndices[0]) - 2 + for i, baseIndex := range baseIndices { + powerStat := stats.ArcanePower + stats.Stat(baseIndex) - 2 + if powerStat == stats.SpellPower { + caster.PseudoStats.BonusDamage = highestValue - 25.0*float64(i) + } else { + caster.stats[powerStat] = highestValue - 25.0*float64(i) + } } } - } else { - highestStat = stats.ArcanePower + stats.Stat(baseIndices[0]) - 2 - for i, baseIndex := range baseIndices { + + // Make sure setup is right + var highestFound stats.Stat + highestValFound := 0.0 + for _, baseIndex := range baseIndices { powerStat := stats.ArcanePower + stats.Stat(baseIndex) - 2 if powerStat == stats.SpellPower { - caster.PseudoStats.BonusDamage = highestValue - 25.0*float64(i) + if caster.PseudoStats.BonusDamage > highestValFound { + highestValFound = caster.PseudoStats.BonusDamage + highestFound = powerStat + } } else { - caster.stats[powerStat] = highestValue - 25.0*float64(i) + if caster.GetStat(powerStat) > highestValFound { + highestValFound = caster.GetStat(powerStat) + highestFound = powerStat + } } } - } - - // Make sure setup is right - var highestFound stats.Stat - highestValFound := 0.0 - for _, baseIndex := range baseIndices { - powerStat := stats.ArcanePower + stats.Stat(baseIndex) - 2 - if powerStat == stats.SpellPower { - if caster.PseudoStats.BonusDamage > highestValFound { - highestValFound = caster.PseudoStats.BonusDamage - highestFound = powerStat - } - } else { - if caster.GetStat(powerStat) > highestValFound { - highestValFound = caster.GetStat(powerStat) - highestFound = powerStat - } + if highestFound != highestStat || highestValFound != highestValue { + t.Errorf("Expected power %d to be highest with %f, but found %d at %f to be highest school power for school %d!", highestStat, highestValue, highestFound, highestValFound, schoolMask) + return } - } - if highestFound != highestStat || highestValFound != highestValue { - t.Errorf("Expected power %d to be highest with %f, but found %d at %f to be highest school power for index %d!", highestStat, highestValue, highestFound, highestValFound, schoolIndex) - return - } - power := spell.SpellSchoolPower() - if power != highestValue { - t.Errorf("Expected %f to be highest power value found, but got %f for index %d!", highestValue, power, schoolIndex) - return + power := spell.SpellSchoolPower() + if power != highestValue { + t.Errorf("Expected %f to be highest power value found, but got %f for school %d!", highestValue, power, schoolMask) + return + } } } } @@ -248,8 +300,8 @@ func Test_MultiSchoolSpellPower(t *testing.T) { const highestMult float64 = 5.0 -func SchoolMultiplierArrayHelper(t *testing.T, caster *Unit, target *Unit, multArray *[stats.PrimarySchoolLen]float64, - testFunc func(spell *Spell, schoolIndex stats.SchoolIndex) (bool, string)) { +func SchoolMultiplierArrayHelper(t *testing.T, caster *Unit, target *Unit, multArray *[stats.SchoolLen]float64, + testFunc func(spell *Spell, schoolMask SpellSchool) (bool, string)) { spell := &Spell{ DamageMultiplier: 1, @@ -257,18 +309,27 @@ func SchoolMultiplierArrayHelper(t *testing.T, caster *Unit, target *Unit, multA Unit: caster, } - for schoolIndex := stats.SchoolIndexSpellstrike; schoolIndex < stats.SchoolLen; schoolIndex++ { - spell.SetSchool(schoolIndex) + for schoolIndex1 := stats.SchoolIndexPhysical; schoolIndex1 < stats.SchoolLen; schoolIndex1++ { + for schoolIndex2 := stats.SchoolIndexPhysical; schoolIndex2 < stats.SchoolLen; schoolIndex2++ { + if schoolIndex2 == schoolIndex1 { + continue + } - for i, baseIndex := range spell.SchoolBaseIndices { - multArray[baseIndex] = highestMult - float64(i)*0.5 - } + schoolMask := SpellSchoolFromIndex(schoolIndex1) | SpellSchoolFromIndex(schoolIndex2) + spell.SpellSchool = schoolMask + spell.SchoolIndex = schoolMask.GetSchoolIndex() + spell.SchoolBaseIndices = schoolMask.GetBaseIndices() - ok, errMsg := testFunc(spell, schoolIndex) + for i, baseIndex := range spell.SchoolBaseIndices { + multArray[baseIndex] = highestMult - float64(i)*0.5 + } - if !ok { - t.Error(errMsg) - return + ok, errMsg := testFunc(spell, schoolMask) + + if !ok { + t.Error(errMsg) + return + } } } } @@ -292,10 +353,10 @@ func Test_MultiSchoolModifiers(t *testing.T) { t.Run("DamageDealt", func(t *testing.T) { SchoolMultiplierArrayHelper(t, caster, target, &caster.PseudoStats.SchoolDamageDealtMultiplier, - func(spell *Spell, schoolIndex stats.SchoolIndex) (bool, string) { + func(spell *Spell, schoolMask SpellSchool) (bool, string) { mult := spell.AttackerDamageMultiplier(attackTable) if mult != highestMult { - return false, fmt.Sprintf("Damage dealt multiplier for school %d returned %f, expected %f!", schoolIndex, mult, highestMult) + return false, fmt.Sprintf("Damage dealt multiplier for school %d returned %f, expected %f!", schoolMask, mult, highestMult) } return true, "" }) @@ -303,10 +364,10 @@ func Test_MultiSchoolModifiers(t *testing.T) { t.Run("DamageTaken", func(t *testing.T) { SchoolMultiplierArrayHelper(t, caster, target, &target.PseudoStats.SchoolDamageTakenMultiplier, - func(spell *Spell, schoolIndex stats.SchoolIndex) (bool, string) { + func(spell *Spell, schoolMask SpellSchool) (bool, string) { mult := spell.TargetDamageMultiplier(attackTable, false) if mult != highestMult { - return false, fmt.Sprintf("Damage taken multiplier for school %d returned %f, expected %f!", schoolIndex, mult, highestMult) + return false, fmt.Sprintf("Damage taken multiplier for school %d returned %f, expected %f!", schoolMask, mult, highestMult) } return true, "" }) diff --git a/sim/core/stats/stats.go b/sim/core/stats/stats.go index fe73ba169c..50b8f53d6f 100644 --- a/sim/core/stats/stats.go +++ b/sim/core/stats/stats.go @@ -110,53 +110,19 @@ const ( SchoolIndexNature SchoolIndexShadow - PrimarySchoolLen - - // Physical x Other - SchoolIndexSpellstrike SchoolIndex = iota - 1 - SchoolIndexFlamestrike - SchoolIndexFroststrike - SchoolIndexHolystrike - SchoolIndexStormstrike - SchoolIndexShadowstrike - - // Arcane x Other - SchoolIndexSpellfire - SchoolIndexSpellFrost - SchoolIndexDivine - SchoolIndexAstral - SchoolIndexSpellShadow - - // Fire x Other - SchoolIndexFrostfire - SchoolIndexRadiant - SchoolIndexVolcanic - SchoolIndexShadowflame - - // Frost x Other - SchoolIndexHolyfrost - SchoolIndexFroststorm - SchoolIndexShadowfrost - - // Holy x Other - SchoolIndexHolystorm - SchoolIndexTwilight - - // Nature x Other - SchoolIndexPlague - - SchoolIndexElemental - SchoolLen + + // School is composed of multiple base schools. + SchoolIndexMultischool SchoolIndex = iota - 1 // This is deliberately set this way to be a continuous sequence. ) // Check if school index is a multi-school. func (schoolIndex SchoolIndex) IsMultiSchool() bool { - return schoolIndex >= PrimarySchoolLen + return schoolIndex == SchoolIndexMultischool } -func NewSchoolFloatArray() [PrimarySchoolLen]float64 { - return [PrimarySchoolLen]float64{ +func NewSchoolFloatArray() [SchoolLen]float64 { + return [SchoolLen]float64{ 1, 1, 1, 1, 1, 1, 1, 1, } } @@ -413,8 +379,8 @@ type PseudoStats struct { ThreatMultiplier float64 // Modulates the threat generated. Affected by things like salv. - DamageDealtMultiplier float64 // All damage - SchoolDamageDealtMultiplier [PrimarySchoolLen]float64 // For specific spell schools. DO NOT use with multi school idices! See helper functions on Unit! + DamageDealtMultiplier float64 // All damage + SchoolDamageDealtMultiplier [SchoolLen]float64 // For specific spell schools. DO NOT use with multi school idices! See helper functions on Unit! // Treat melee haste as a pseudostat so that shamans, paladins, and druids can get the correct scaling MeleeHasteRatingPerHastePercent float64 @@ -472,9 +438,9 @@ type PseudoStats struct { BonusPhysicalDamageTaken float64 // Hemo, Gift of Arthas, etc BonusHealingTaken float64 // Talisman of Troll Divinity - DamageTakenMultiplier float64 // All damage - SchoolDamageTakenMultiplier [PrimarySchoolLen]float64 // For specific spell schools. DO NOT use with multi school idices! See helper functions on Unit! - SchoolCritTakenMultiplier [PrimarySchoolLen]float64 // For spell school crit. DO NOT use with multi school idices! See helper functions on Unit! + DamageTakenMultiplier float64 // All damage + SchoolDamageTakenMultiplier [SchoolLen]float64 // For specific spell schools. DO NOT use with multi school idices! See helper functions on Unit! + SchoolCritTakenMultiplier [SchoolLen]float64 // For spell school crit. DO NOT use with multi school idices! See helper functions on Unit! BleedDamageTakenMultiplier float64 // Modifies damage taken from bleed effects DiseaseDamageTakenMultiplier float64 // Modifies damage taken from disease effects From ef88fcf4316217625f5c550690581b3ea23e6e02 Mon Sep 17 00:00:00 2001 From: FelixPflaum <141590183+FelixPflaum@users.noreply.github.com> Date: Sat, 23 Mar 2024 21:30:19 +0100 Subject: [PATCH 3/6] Fix school crit taken, remove unused crit taken stats --- sim/core/debuffs.go | 6 +++--- sim/core/spell_result.go | 11 +++++------ sim/core/spell_school.go | 9 ++++----- sim/core/stats/stats.go | 17 ++++++++--------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/sim/core/debuffs.go b/sim/core/debuffs.go index bc56b6af86..3ba044266b 100644 --- a/sim/core/debuffs.go +++ b/sim/core/debuffs.go @@ -588,11 +588,11 @@ func WintersChillAura(target *Unit, startingStacks int32) *Aura { aura.SetStacks(sim, startingStacks) }, OnStacksChange: func(aura *Aura, sim *Simulation, oldStacks, newStacks int32) { - aura.Unit.PseudoStats.SchoolCritTakenMultiplier[stats.SchoolIndexFrost] /= 1 + 0.2*float64(oldStacks) - aura.Unit.PseudoStats.SchoolCritTakenMultiplier[stats.SchoolIndexFrost] *= 1 + 0.2*float64(newStacks) + aura.Unit.PseudoStats.SchoolCritTakenChance[stats.SchoolIndexFrost] -= 0.02 * float64(oldStacks) + aura.Unit.PseudoStats.SchoolCritTakenChance[stats.SchoolIndexFrost] += 0.02 * float64(newStacks) }, OnExpire: func(aura *Aura, sim *Simulation) { - aura.Unit.PseudoStats.SchoolCritTakenMultiplier[stats.SchoolIndexFrost] /= 1 + 0.2*float64(aura.stacks) + aura.Unit.PseudoStats.SchoolCritTakenChance[stats.SchoolIndexFrost] -= 0.02 * float64(aura.stacks) }, }) diff --git a/sim/core/spell_result.go b/sim/core/spell_result.go index 552894cb32..99f4a633c8 100644 --- a/sim/core/spell_result.go +++ b/sim/core/spell_result.go @@ -99,8 +99,7 @@ func (spell *Spell) PhysicalHitChance(attackTable *AttackTable) float64 { func (spell *Spell) PhysicalCritChance(attackTable *AttackTable) float64 { critRating := spell.Unit.stats[stats.MeleeCrit] + - spell.BonusCritRating + - attackTable.Defender.PseudoStats.BonusCritRatingTaken + spell.BonusCritRating return critRating/(CritRatingPerCritChance*100) - attackTable.MeleeCritSuppression } func (spell *Spell) PhysicalCritCheck(sim *Simulation, attackTable *AttackTable) bool { @@ -193,13 +192,13 @@ func (spell *Spell) MagicHitCheck(sim *Simulation, attackTable *AttackTable) boo func (spell *Spell) spellCritRating(target *Unit) float64 { return spell.Unit.stats[stats.SpellCrit] + - spell.BonusCritRating + - target.PseudoStats.BonusCritRatingTaken + - target.PseudoStats.BonusSpellCritRatingTaken + spell.BonusCritRating } func (spell *Spell) SpellCritChance(target *Unit) float64 { // TODO: Classic verify crit suppression - return spell.spellCritRating(target) / (SpellCritRatingPerCritChance * 100) // - spell.Unit.AttackTables[target.UnitIndex][spell.CastType].SpellCritSuppression + return spell.spellCritRating(target)/(SpellCritRatingPerCritChance*100) + + target.GetSchoolCritTakenChance(spell) + // - spell.Unit.AttackTables[target.UnitIndex][spell.CastType].SpellCritSuppression } func (spell *Spell) MagicCritCheck(sim *Simulation, target *Unit) bool { critChance := spell.SpellCritChance(target) diff --git a/sim/core/spell_school.go b/sim/core/spell_school.go index 0c56b4fdfc..097aee85ef 100644 --- a/sim/core/spell_school.go +++ b/sim/core/spell_school.go @@ -166,11 +166,10 @@ func (unit *Unit) GetSchoolDamageTakenMultiplier(spell *Spell) float64 { return selectMaxMultInSchoolArray(spell, &unit.PseudoStats.SchoolDamageTakenMultiplier) } -// Get school crit taken multiplier. -// Returns highest multiplier if spell is multi school. -func (unit *Unit) GetCritTakenMultiplier(spell *Spell) float64 { +// Returns highest if spell is multi school. +func (unit *Unit) GetSchoolCritTakenChance(spell *Spell) float64 { if !spell.SchoolIndex.IsMultiSchool() { - return unit.PseudoStats.SchoolCritTakenMultiplier[spell.SchoolIndex] + return unit.PseudoStats.SchoolCritTakenChance[spell.SchoolIndex] } - return selectMaxMultInSchoolArray(spell, &unit.PseudoStats.SchoolCritTakenMultiplier) + return selectMaxMultInSchoolArray(spell, &unit.PseudoStats.SchoolCritTakenChance) } diff --git a/sim/core/stats/stats.go b/sim/core/stats/stats.go index 50b8f53d6f..5239639f37 100644 --- a/sim/core/stats/stats.go +++ b/sim/core/stats/stats.go @@ -121,9 +121,10 @@ func (schoolIndex SchoolIndex) IsMultiSchool() bool { return schoolIndex == SchoolIndexMultischool } -func NewSchoolFloatArray() [SchoolLen]float64 { +func NewSchoolFloatArray(defaultVal float64) [SchoolLen]float64 { + d := defaultVal return [SchoolLen]float64{ - 1, 1, 1, 1, 1, 1, 1, 1, + d, d, d, d, d, d, d, d, } } @@ -430,8 +431,6 @@ type PseudoStats struct { ReducedCritTakenChance float64 // Reduces chance to be crit. BonusRangedAttackPowerTaken float64 // Hunters mark - BonusSpellCritRatingTaken float64 // Imp Shadow Bolt / Imp Scorch / Winter's Chill debuff - BonusCritRatingTaken float64 // Totem of Wrath / Master Poisoner / Heart of the Crusader BonusMeleeHitRatingTaken float64 // Formerly Imp FF and SW Radiance; BonusSpellHitRatingTaken float64 // Imp FF @@ -439,8 +438,8 @@ type PseudoStats struct { BonusHealingTaken float64 // Talisman of Troll Divinity DamageTakenMultiplier float64 // All damage - SchoolDamageTakenMultiplier [SchoolLen]float64 // For specific spell schools. DO NOT use with multi school idices! See helper functions on Unit! - SchoolCritTakenMultiplier [SchoolLen]float64 // For spell school crit. DO NOT use with multi school idices! See helper functions on Unit! + SchoolDamageTakenMultiplier [SchoolLen]float64 // For specific spell schools. DO NOT use with multi school index! See helper functions on Unit! + SchoolCritTakenChance [SchoolLen]float64 // For spell school crit. DO NOT use with multi school index! See helper functions on Unit! BleedDamageTakenMultiplier float64 // Modifies damage taken from bleed effects DiseaseDamageTakenMultiplier float64 // Modifies damage taken from disease effects @@ -464,7 +463,7 @@ func NewPseudoStats() PseudoStats { ThreatMultiplier: 1, DamageDealtMultiplier: 1, - SchoolDamageDealtMultiplier: NewSchoolFloatArray(), + SchoolDamageDealtMultiplier: NewSchoolFloatArray(1), MeleeHasteRatingPerHastePercent: 1, @@ -474,8 +473,8 @@ func NewPseudoStats() PseudoStats { // Target effects. DamageTakenMultiplier: 1, - SchoolDamageTakenMultiplier: NewSchoolFloatArray(), - SchoolCritTakenMultiplier: NewSchoolFloatArray(), + SchoolDamageTakenMultiplier: NewSchoolFloatArray(1), + SchoolCritTakenChance: NewSchoolFloatArray(0), BleedDamageTakenMultiplier: 1, DiseaseDamageTakenMultiplier: 1, From bc32d7cd7b432c29c259432da9ecd05146c25f1d Mon Sep 17 00:00:00 2001 From: FelixPflaum <141590183+FelixPflaum@users.noreply.github.com> Date: Sat, 23 Mar 2024 21:34:26 +0100 Subject: [PATCH 4/6] Update test results --- sim/mage/TestFrostFire.results | 58 ++++++++++++++-------------- sim/priest/shadow/TestShadow.results | 56 +++++++++++++-------------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/sim/mage/TestFrostFire.results b/sim/mage/TestFrostFire.results index dbf99e86b6..712a1da888 100644 --- a/sim/mage/TestFrostFire.results +++ b/sim/mage/TestFrostFire.results @@ -53,18 +53,18 @@ stat_weights_results: { weights: 0 weights: 0 weights: 0 - weights: 0.67798 + weights: -0.41786 weights: 0 - weights: 0.77698 + weights: 0.82591 weights: 0 - weights: 0.77698 + weights: 0.82591 weights: 0 weights: 0 weights: 0 weights: 0 weights: 0 - weights: 3.22562 - weights: 6.08707 + weights: 4.27657 + weights: 1.54641 weights: 0 weights: 0 weights: 0 @@ -99,57 +99,57 @@ stat_weights_results: { dps_results: { key: "TestFrostFire-Lvl40-AllItems-HyperconductiveMender'sMeditation" value: { - dps: 394.89206 - tps: 285.65145 + dps: 421.88494 + tps: 305.42857 } } dps_results: { key: "TestFrostFire-Lvl40-AllItems-HyperconductiveWizard'sAttire" value: { - dps: 430.64958 - tps: 311.51058 + dps: 461.815 + tps: 333.67134 } } dps_results: { key: "TestFrostFire-Lvl40-AllItems-IrradiatedGarments" value: { - dps: 442.98076 - tps: 320.21088 + dps: 472.82642 + tps: 340.99936 } } dps_results: { key: "TestFrostFire-Lvl40-AllItems-TwilightInvoker'sVestments" value: { - dps: 412.70793 - tps: 299.26404 + dps: 438.01562 + tps: 317.22767 } } dps_results: { key: "TestFrostFire-Lvl40-Average-Default" value: { - dps: 543.03976 - tps: 391.28136 + dps: 581.09147 + tps: 418.29913 } } dps_results: { key: "TestFrostFire-Lvl40-Settings-Gnome-p2_frostfire-Frostfire-p2_frostfire-FullBuffs-Phase 2 Consumes-LongMultiTarget" value: { - dps: 1206.60284 - tps: 1064.78322 + dps: 1263.05695 + tps: 1114.55904 } } dps_results: { key: "TestFrostFire-Lvl40-Settings-Gnome-p2_frostfire-Frostfire-p2_frostfire-FullBuffs-Phase 2 Consumes-LongSingleTarget" value: { - dps: 534.40353 - tps: 384.96152 + dps: 571.15388 + tps: 411.25649 } } dps_results: { key: "TestFrostFire-Lvl40-Settings-Gnome-p2_frostfire-Frostfire-p2_frostfire-FullBuffs-Phase 2 Consumes-ShortSingleTarget" value: { - dps: 575.82491 - tps: 416.1578 + dps: 614.12017 + tps: 443.47201 } } dps_results: { @@ -176,22 +176,22 @@ dps_results: { dps_results: { key: "TestFrostFire-Lvl40-Settings-Troll-p2_frostfire-Frostfire-p2_frostfire-FullBuffs-Phase 2 Consumes-LongMultiTarget" value: { - dps: 1175.99959 - tps: 1040.88191 + dps: 1221.9566 + tps: 1083.73056 } } dps_results: { key: "TestFrostFire-Lvl40-Settings-Troll-p2_frostfire-Frostfire-p2_frostfire-FullBuffs-Phase 2 Consumes-LongSingleTarget" value: { - dps: 537.15263 - tps: 387.18277 + dps: 575.12627 + tps: 414.07501 } } dps_results: { key: "TestFrostFire-Lvl40-Settings-Troll-p2_frostfire-Frostfire-p2_frostfire-FullBuffs-Phase 2 Consumes-ShortSingleTarget" value: { - dps: 580.47787 - tps: 419.38501 + dps: 616.22633 + tps: 444.90372 } } dps_results: { @@ -218,7 +218,7 @@ dps_results: { dps_results: { key: "TestFrostFire-Lvl40-SwitchInFrontOfTarget-Default" value: { - dps: 539.40248 - tps: 388.76615 + dps: 575.54722 + tps: 414.38491 } } diff --git a/sim/priest/shadow/TestShadow.results b/sim/priest/shadow/TestShadow.results index 2f97a5e3bc..5c82fbfe61 100644 --- a/sim/priest/shadow/TestShadow.results +++ b/sim/priest/shadow/TestShadow.results @@ -151,18 +151,18 @@ stat_weights_results: { weights: 0 weights: 0 weights: 0 - weights: 0.55792 + weights: 0.52939 weights: 0 - weights: 0.54754 + weights: 0.55889 weights: 0 weights: 0 weights: 0 weights: 0 weights: 0 - weights: 0.54754 + weights: 0.55889 weights: 0 weights: 0.12662 - weights: 0.79653 + weights: 0.88594 weights: 0 weights: 0 weights: 0 @@ -323,57 +323,57 @@ dps_results: { dps_results: { key: "TestShadow-Lvl40-AllItems-HyperconductiveMender'sMeditation" value: { - dps: 338.94317 - tps: 289.89917 + dps: 347.67168 + tps: 297.23112 } } dps_results: { key: "TestShadow-Lvl40-AllItems-HyperconductiveWizard'sAttire" value: { - dps: 350.36765 - tps: 298.48857 + dps: 358.9555 + tps: 305.70236 } } dps_results: { key: "TestShadow-Lvl40-AllItems-IrradiatedGarments" value: { - dps: 350.08365 - tps: 298.13068 + dps: 359.17159 + tps: 305.76455 } } dps_results: { key: "TestShadow-Lvl40-AllItems-TwilightInvoker'sVestments" value: { - dps: 348.68083 - tps: 266.78862 + dps: 356.15608 + tps: 273.06783 } } dps_results: { key: "TestShadow-Lvl40-Average-Default" value: { - dps: 490.29411 - tps: 390.46699 + dps: 497.09925 + tps: 396.1833 } } dps_results: { key: "TestShadow-Lvl40-Settings-NightElf-phase_2-Basic-phase_2-FullBuffs-Phase 2 Consumes-LongMultiTarget" value: { - dps: 512.94021 - tps: 724.44789 + dps: 520.51227 + tps: 730.80841 } } dps_results: { key: "TestShadow-Lvl40-Settings-NightElf-phase_2-Basic-phase_2-FullBuffs-Phase 2 Consumes-LongSingleTarget" value: { - dps: 512.94021 - tps: 409.6102 + dps: 520.51227 + tps: 415.97072 } } dps_results: { key: "TestShadow-Lvl40-Settings-NightElf-phase_2-Basic-phase_2-FullBuffs-Phase 2 Consumes-ShortSingleTarget" value: { - dps: 609.29338 - tps: 494.65536 + dps: 618.38021 + tps: 502.28829 } } dps_results: { @@ -400,22 +400,22 @@ dps_results: { dps_results: { key: "TestShadow-Lvl40-Settings-Undead-phase_2-Basic-phase_2-FullBuffs-Phase 2 Consumes-LongMultiTarget" value: { - dps: 492.19979 - tps: 701.28671 + dps: 499.5538 + tps: 707.46408 } } dps_results: { key: "TestShadow-Lvl40-Settings-Undead-phase_2-Basic-phase_2-FullBuffs-Phase 2 Consumes-LongSingleTarget" value: { - dps: 492.19979 - tps: 391.95617 + dps: 499.5538 + tps: 398.13354 } } dps_results: { key: "TestShadow-Lvl40-Settings-Undead-phase_2-Basic-phase_2-FullBuffs-Phase 2 Consumes-ShortSingleTarget" value: { - dps: 618.73165 - tps: 503.50339 + dps: 627.3699 + tps: 510.75952 } } dps_results: { @@ -442,7 +442,7 @@ dps_results: { dps_results: { key: "TestShadow-Lvl40-SwitchInFrontOfTarget-Default" value: { - dps: 492.19979 - tps: 391.95617 + dps: 499.5538 + tps: 398.13354 } } From 8a66ffb1f5ac8dd05e4b1daab61e4946e523d74d Mon Sep 17 00:00:00 2001 From: FelixPflaum <141590183+FelixPflaum@users.noreply.github.com> Date: Sat, 23 Mar 2024 22:20:13 +0100 Subject: [PATCH 5/6] Add test case for crit chance taken --- sim/core/spell_school_test.go | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/sim/core/spell_school_test.go b/sim/core/spell_school_test.go index 5876fb2b28..54cde45d1e 100644 --- a/sim/core/spell_school_test.go +++ b/sim/core/spell_school_test.go @@ -373,17 +373,14 @@ func Test_MultiSchoolModifiers(t *testing.T) { }) }) - // TODO: Test for crit taken, it's currently not used anywhere. - - // t.Run("CritTaken", func(t *testing.T) { - // SchoolMultiplierArrayHelper(t, caster, target, &target.PseudoStats.SchoolCritTakenMultiplier, - // func(spell *Spell, schoolIndex stats.SchoolIndex) (bool, string) { - // spell.MultiSchoolUpdateModifiers(target) - // mult := spell.TargetDamageMultiplier(attackTable, false) - // if mult != highestMult { - // return false, fmt.Sprintf("Damage taken multiplier for school %d returned %f, expected %f!", schoolIndex, mult, highestMult) - // } - // return true, "" - // }) - // }) + t.Run("CritChanceTaken", func(t *testing.T) { + SchoolMultiplierArrayHelper(t, caster, target, &target.PseudoStats.SchoolCritTakenChance, + func(spell *Spell, schoolMask SpellSchool) (bool, string) { + critChance := spell.SpellCritChance(target) + if critChance != highestMult { + return false, fmt.Sprintf("Crit chance taken for school %d returned %f, expected %f!", schoolMask, critChance, highestMult) + } + return true, "" + }) + }) } From 995fbe1bdd54ef55075774d8cbb46605f96e8916 Mon Sep 17 00:00:00 2001 From: FelixPflaum <141590183+FelixPflaum@users.noreply.github.com> Date: Wed, 27 Mar 2024 01:35:00 +0100 Subject: [PATCH 6/6] Fix missing rename --- sim/core/spell_school_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sim/core/spell_school_test.go b/sim/core/spell_school_test.go index 6026a154e4..0fd60d6f4c 100644 --- a/sim/core/spell_school_test.go +++ b/sim/core/spell_school_test.go @@ -12,7 +12,7 @@ import ( func BenchmarkMultiSchoolMultipliers(b *testing.B) { school := SpellSchoolFrost | SpellSchoolShadow - multipliers := [stats.PrimarySchoolLen]float64{ + multipliers := [stats.SchoolLen]float64{ stats.SchoolIndexNone: 1, stats.SchoolIndexPhysical: 1, stats.SchoolIndexArcane: 1.1,